Google Places API Nearby Search 未返回结果
数据小许
结论
问题出在请求 URL 的构造上。代码将纬度写成了字符串常量 "latitude",没有使用函数参数 latitude。
当前代码实际发送的坐标类似:
location=latitude,-122.6628821
正确的坐标应该是:
location=45.5662453,-122.6628821
Google Places API 因此收到了无效坐标。XML 解析路径 //status 和 //result/name 通常适用于该响应结构,不是问题的主要原因。
修正请求构造
将:
"location=" & "latitude" & "," & longitude
改为:
"location=" & latitude & "," & longitude
最小修改如下:
xmlhttpRequest.Open "GET", _
"https://maps.googleapis.com/maps/api/place/nearbysearch/xml?" & _
"location=" & latitude & "," & longitude & _
"&radius=" & Radius & _
"&type=park" & _
"&key=" & apiKey, _
False
latitude 不能放在双引号内。双引号中的内容是固定文本,不会被当作变量名解析。
更稳妥的实现
VBA 将 Double 转换成字符串时,会受到 Windows 区域设置的影响。某些区域使用逗号作为小数分隔符,生成的内容可能如下:
location=45,5662453,-122,6628821
这样的坐标同样无效。可以显式将小数分隔符替换为英文句点:
Public Function GetNearbyPark( _
ByVal latitude As Double, _
ByVal longitude As Double, _
ByVal Radius As Long) As String
Dim apiKey As String
Dim requestUrl As String
Dim latitudeText As String
Dim longitudeText As String
Dim xmlhttpRequest As Object
Dim xmlDoc As Object
Dim xmlStatusNode As Object
Dim xmlNearbyParkNameNode As Object
On Error GoTo errorHandler
apiKey = "YOUR_API_KEY"
If Len(apiKey) = 0 Or apiKey = "YOUR_API_KEY" Then
GetNearbyPark = "Empty or invalid API Key"
Exit Function
End If
latitudeText = Replace$(CStr(latitude), _
Application.International(xlDecimalSeparator), ".")
longitudeText = Replace$(CStr(longitude), _
Application.International(xlDecimalSeparator), ".")
requestUrl = _
"https://maps.googleapis.com/maps/api/place/nearbysearch/xml?" & _
"location=" & latitudeText & "," & longitudeText & _
"&radius=" & CStr(Radius) & _
"&type=park" & _
"&key=" & apiKey
Debug.Print requestUrl
Set xmlhttpRequest = CreateObject("MSXML2.ServerXMLHTTP.6.0")
xmlhttpRequest.Open "GET", requestUrl, False
xmlhttpRequest.send
If xmlhttpRequest.Status <> 200 Then
GetNearbyPark = "HTTP error: " & xmlhttpRequest.Status & _
" " & xmlhttpRequest.statusText
GoTo cleanUp
End If
Set xmlDoc = CreateObject("MSXML2.DOMDocument.6.0")
xmlDoc.async = False
xmlDoc.validateOnParse = False
If Not xmlDoc.LoadXML(xmlhttpRequest.responseText) Then
GetNearbyPark = "Invalid XML response: " & _
xmlDoc.parseError.reason
GoTo cleanUp
End If
Set xmlStatusNode = xmlDoc.SelectSingleNode("/PlaceSearchResponse/status")
If xmlStatusNode Is Nothing Then
GetNearbyPark = "The response does not contain a status node"
GoTo cleanUp
End If
Select Case UCase$(xmlStatusNode.Text)
Case "OK"
Set xmlNearbyParkNameNode = _
xmlDoc.SelectSingleNode("/PlaceSearchResponse/result[1]/name")
If xmlNearbyParkNameNode Is Nothing Then
GetNearbyPark = "The response contains no park name"
Else
GetNearbyPark = xmlNearbyParkNameNode.Text
End If
Case "ZERO_RESULTS"
GetNearbyPark = _
"No park exists within the radius of the defined coordinates"
Case "OVER_DAILY_LIMIT"
GetNearbyPark = "Billing, API key, or usage limit problem"
Case "OVER_QUERY_LIMIT"
GetNearbyPark = "Quota limit exceeded"
Case "REQUEST_DENIED"
GetNearbyPark = "Server denied the request"
Case "INVALID_REQUEST"
GetNearbyPark = "Request was empty or malformed"
Case "UNKNOWN_ERROR"
GetNearbyPark = "Unknown server error"
Case Else
GetNearbyPark = "API status: " & xmlStatusNode.Text
End Select
cleanUp:
Set xmlNearbyParkNameNode = Nothing
Set xmlStatusNode = Nothing
Set xmlDoc = Nothing
Set xmlhttpRequest = Nothing
Exit Function
errorHandler:
GetNearbyPark = "VBA error " & Err.Number & ": " & Err.Description
Resume cleanUp
End Function
XML 解析是否有问题
下面的 XPath 可以取得第一个结果的名称:
xmlDoc.SelectSingleNode("//result/name")
使用完整路径会更明确:
xmlDoc.SelectSingleNode("/PlaceSearchResponse/result[1]/name")
需要补强的是空节点检查。请求失败、响应格式改变或没有结果时,SelectSingleNode 可能返回 Nothing。如果此时直接访问:
xmlStatusNode.Text
或:
xmlNearbyParkNameNode.Text
就会触发对象变量错误。原代码虽然使用了 On Error GoTo errorHandler,但错误处理程序没有将错误信息赋给函数返回值,所以表面上看起来像是“没有返回结果”。
排查方法
先输出 VBA 实际发送的完整 URL:
Debug.Print requestUrl
将输出内容复制到浏览器中测试,并逐项确认:
location中是实际数值,不是单词latitude;- 纬度和经度之间只有一个逗号;
- 小数分隔符是
.; - HTTP 状态码为
200; - XML 中的
<status>是OK; - Places API 已启用,项目的结算、配额和 API key 限制也已正确配置。
安全注意事项
问题中公开了真实 API key,应将该密钥视为已经泄露。建议立即在 Google Cloud Console 中撤销或轮换密钥,并为新密钥设置适当的 API 限制和应用限制。不要继续在公开代码、日志或网页中使用该密钥。
备注:内容仅供参考。
