VBA 无法读取 OpenStreetMap API 的 XML 结果
结论
<place> 节点并没有被截断,也不是因为 xmlDoc.Load 只能加载两层。实际响应中的 <searchresults> 是空元素,里面没有 <place>。
出现这种情况有两个原因:
- 查询地址使用了 Nominatim 无法识别的缩写
gm.,因此没有查到结果。 - 地址中的波兰语字符未按 UTF-8 正确进行 URL 编码。响应里的
KoĹ›cielne和GĹ‚usk表明字符编码有误。
把查询内容改为 Abramowice Kościelne Głusk,再对参数进行 UTF-8 URL 编码,就能通过 XPath 读取第一个 <place>。经纬度保存在 <place> 的 lat 和 lon 属性中,不是它的子节点:
/searchresults/place[1]/@lat
/searchresults/place[1]/@lon
Load 和 LoadXML 的区别
这两个方法的用途不同,不能互换:
xmlDoc.LoadXML xmlhttpRequest.responseText
LoadXML 接收 XML 字符串。
xmlDoc.Load xmlhttpRequest.responseXML
Load 通常接收文件名、URL、流或其他可加载的数据源,不适合传入已经生成的 DOM 文档。处理 HTTP 响应时,可以直接使用 responseXML:
Set xmlDoc = xmlhttpRequest.responseXML
也可以继续使用:
xmlDoc.LoadXML xmlhttpRequest.responseText
调试器中显示 document 的直接子节点只有 XML 声明和 <searchresults>,这是正常现象。<place> 位于 <searchresults> 下,并不是 document 的直接子节点:
DOMDocument
├── <?xml ...?>
└── <searchresults>
└── <place>
要查看 <searchresults> 下的节点,可以检查 xmlDoc.documentElement.childNodes。
可用的 VBA 实现
下面的代码按照 Nominatim 的 XML 响应格式读取经纬度,同时处理 HTTP 错误、XML 解析失败和查询无结果等情况:
Public Function GetCoordinates(ByVal address As String) As String
Dim http As Object
Dim xmlDoc As Object
Dim placeNode As Object
Dim requestUrl As String
On Error GoTo ErrorHandler
requestUrl = "https://nominatim.openstreetmap.org/search" & _
"?q=" & UrlEncodeUTF8(address) & _
"&format=xml" & _
"&addressdetails=1" & _
"&limit=1"
Set http = CreateObject("MSXML2.ServerXMLHTTP.6.0")
http.Open "GET", requestUrl, False
http.setRequestHeader "User-Agent", _
"AccessGeocoder/1.0 (contact: your-email@example.com)"
http.setRequestHeader "Accept", "application/xml"
http.send
If http.Status <> 200 Then
GetCoordinates = "HTTP error: " & http.Status & " " & http.statusText
GoTo CleanUp
End If
Set xmlDoc = CreateObject("MSXML2.DOMDocument.6.0")
xmlDoc.async = False
xmlDoc.validateOnParse = False
If Not xmlDoc.LoadXML(http.responseText) Then
GetCoordinates = "Invalid XML: " & xmlDoc.parseError.reason
GoTo CleanUp
End If
Set placeNode = xmlDoc.SelectSingleNode("/searchresults/place[1]")
If placeNode Is Nothing Then
GetCoordinates = "No result found"
GoTo CleanUp
End If
GetCoordinates = placeNode.getAttribute("lat") & ", " & _
placeNode.getAttribute("lon")
CleanUp:
Set placeNode = Nothing
Set xmlDoc = Nothing
Set http = Nothing
Exit Function
ErrorHandler:
GetCoordinates = "Error " & Err.Number & ": " & Err.Description
Resume CleanUp
End Function
代码不再查找 Google 响应中的这些节点:
//statusText
//result/geometry/location/lat
//result/geometry/location/lng
Nominatim 的 XML 结构与 Google 完全不同,其中也没有 Google 风格的 statusText。
UTF-8 URL 编码函数
只执行下面的替换:
Replace(address, " ", "+")
无法正确编码 ś、ł 等非 ASCII 字符。下面的函数会先把文本转换为 UTF-8 字节,再进行百分号编码:
Private Function UrlEncodeUTF8(ByVal value As String) As String
Dim stream As Object
Dim bytes() As Byte
Dim i As Long
Dim b As Long
Dim result As String
Set stream = CreateObject("ADODB.Stream")
With stream
.Type = 2
.Charset = "utf-8"
.Open
.WriteText value
.Position = 0
.Type = 1
.Position = 3 '跳过 UTF-8 BOM
bytes = .Read
.Close
End With
For i = LBound(bytes) To UBound(bytes)
b = bytes(i)
Select Case b
Case 48 To 57, 65 To 90, 97 To 122, 45, 46, 95, 126
result = result & Chr$(b)
Case 32
result = result & "%20"
Case Else
result = result & "%" & Right$("0" & Hex$(b), 2)
End Select
Next i
UrlEncodeUTF8 = result
End Function
例如:
Abramowice Kościelne Głusk
编码结果应包含类似下面的内容:
Abramowice%20Ko%C5%9Bcielne%20G%C5%82usk
如果请求或响应中出现 KoĹ›cielne,通常说明 UTF-8 字节被当成了错误的单字节编码。
注意事项
- 删除 Nominatim 无法识别的本地缩写,或者将其展开。例如,把
gm.换成完整地名,也可以直接省略。 - 使用 HTTPS,并设置能够标识应用和联系人的
User-Agent。 - 经纬度保存在
<place>的lat和lon属性中,不是<lat>、<lng>子元素。 <searchresults></searchresults>是合法且完整的 XML,表示搜索没有返回结果。- 如果
responseXML已经是 DOM,可以直接赋值给xmlDoc。如果使用responseText,则应调用LoadXML。 - 批量处理 Access 地址库时,需要限制请求频率并缓存结果。投入使用前,还要确认当前 Nominatim 服务的使用政策。公共服务不适合不受控的大批量地理编码。
备注:内容仅供参考。
