PAYATHON 2026

VBA 无法读取 OpenStreetMap API 的 XML 结果

技术老齐

结论

<place> 节点并没有被截断,也不是因为 xmlDoc.Load 只能加载两层。实际响应中的 <searchresults> 是空元素,里面没有 <place>。

出现这种情况有两个原因:

  1. 查询地址使用了 Nominatim 无法识别的缩写 gm.,因此没有查到结果。
  2. 地址中的波兰语字符未按 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 服务的使用政策。公共服务不适合不受控的大批量地理编码。

备注:内容仅供参考。