使用 Routes API 试用新一代距离矩阵功能。

使用 Distance Matrix API 网络服务的最佳做法

Google Maps Platform 网络服务是指向 Google 服务的一系列 HTTP 接口,可为您的地图应用提供地理位置数据。

本指南介绍了设置 Web 服务请求和处理服务响应的一些常见做法。如需查看 Distance Matrix API 的完整文档,请参阅开发者指南

什么是网络服务?

Google Maps Platform 网络服务是一个接口,用于从外部服务请求 Maps API 数据,并在地图应用中使用这些数据。根据《Google Maps Platform 服务条款》中的许可限制,这些服务设计为与地图结合使用。

Maps API 网络服务使用对特定网址的 HTTP(S) 请求,并将网址参数和/或 JSON 格式的 POST 数据作为参数传递给服务。通常,这些服务会在响应正文中以 JSON 或 XML 的形式返回数据,供您的应用解析和/或处理。

典型的 Distance Matrix API 请求通常采用以下形式:

https://maps.googleapis.com/maps/api/distancematrix/output?parameters

其中 output 表示响应格式(通常为 jsonxml)。

注意:所有 Distance Matrix API 应用都需要进行身份验证。 详细了解身份验证凭据

SSL/TLS 访问

使用 API 密钥或包含用户数据的所有 Google Maps Platform 请求都需要采用 HTTPS 协议。通过 HTTP 发出的包含敏感数据的请求可能会被拒绝。

构建有效网址

您可能认为“有效”网址不言自明,但实际并非如此。例如,在浏览器地址栏中输入的网址可能包含特殊字符(例如 "上海+中國");浏览器需要先在内部将这些字符转换为其他编码,然后再进行传输。同样,任何生成或接受 UTF-8 输入的代码都可能会将包含 UTF-8 字符的网址视为“有效”,但同样需要先转换这些字符,然后再将其发送给网络服务器。该过程称为网址编码百分号编码

特殊字符

我们之所以需要转换特殊字符,是因为所有网址都需要符合统一资源标识符 (URI) 规范所规定的语法。实际上,这意味着网址必须只包含一个特殊的 ASCII 字符子集:熟悉的字母数字符号以及一些在网址内用作控制字符的预留字符。下表汇总了这些字符:

有效网址字符摘要
字符集字符在网址中的用法
字母数字 a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9 文本字符串、架构用法 (http)、端口 (8080) 等
未预留 - 。~ 文本字符串
预留 ! * ' ( ) ; : @ & = + $ , / ? #% [ ] 控制字符和/或文本字符串

构建有效网址时,您必须确保网址只包含“有效网址字符摘要”表格中显示的那些字符。让网址按照上述字符集使用字符通常会带来两个问题,一个是遗漏问题,一个是替换问题:

  • 您要处理的字符未包含在上述字符集内。例如,外语字符(例如 上海+中國)需要使用上述字符进行编码。按照常见惯例,空格(网址内不允许使用空格)通常也使用加号字符 '+' 表示。
  • 字符以预留字符的形式存在于上述字符集内,但需要按原义使用。例如,? 在网址内用于表示查询字符串的开头;如果您想要使用字符串“? and the Mysterions”,则需要对 '?' 字符进行编码。

所有要进行网址编码的字符都会使用一个 '%' 字符和一个与其 UTF-8 字符对应的两个字符的十六进制值进行编码。例如,UTF-8 中的 上海+中國 在进行网址编码后将变为 %E4%B8%8A%E6%B5%B7%2B%E4%B8%AD%E5%9C%8B。字符串 ? and the Mysterians 在进行网址编码后将变为 %3F+and+the+Mysterians%3F%20and%20the%20Mysterians

需要编码的常见字符

以下是一些必须进行编码的常见字符:

不安全的字符 编码值
空格键 %20
" %22
< %3C
> %3E
# %23
% %25
| %7C

转换您通过用户输入获取的网址有时颇为棘手。例如,用户可能会输入“5th&Main St.”这样的地址。一般而言,您应该根据网址的组成部分进行构建,将所有用户输入都视为原义字符。

此外,对于所有的 Google Maps Platform 网络服务 API 或静态网络 API,网址最多可包含 8192 个字符。对于大多数服务,很少出现接近这一字符数限制的情况。但请注意,某些服务具有的若干参数可能会导致网址较长。

合理使用 Google API

如果 API 客户端设计不当,可能会给互联网和 Google 服务器增加额外的负载。本部分包含 API 客户端的一些最佳做法。遵循这些最佳实践有助于避免您的应用因滥用 API 而遭到屏蔽。

指数后退

在极少数情况下,您的请求处理可能会出错;您可能会收到 4XX 或 5XX HTTP 响应代码,或者 TCP 连接可能会在您的客户端与 Google 服务器之间的某处出现故障。通常,值得重试该请求是因为后续请求可能会在原始失败时成功。但是,切勿简单地反复向 Google 的服务器发出请求。这种循环行为可能会使客户端与 Google 之间的网络过载,从而导致多方出现问题。

更好的方法是在两次尝试之间不断增加延迟时间再试。通常,延迟时间在每次尝试时都会乘以乘法系数,即所谓的指数退避算法

例如,假设某个应用希望向 Time Zone API 发出请求:

https://maps.googleapis.com/maps/api/timezone/json?location=39.6034810,-119.6822510&timestamp=1331161200&key=YOUR_API_KEY

下面的 Python 示例显示了如何通过指数后退发起请求:

import json
import time
import urllib.error
import urllib.parse
import urllib.request

# The maps_key defined below isn't a valid Google Maps API key.
# You need to get your own API key.
# See https://developers.google.com/maps/documentation/timezone/get-api-key
API_KEY = "YOUR_KEY_HERE"
TIMEZONE_BASE_URL = "https://maps.googleapis.com/maps/api/timezone/json"


def timezone(lat, lng, timestamp):

    # Join the parts of the URL together into one string.
    params = urllib.parse.urlencode(
        {"location": f"{lat},{lng}", "timestamp": timestamp, "key": API_KEY,}
    )
    url = f"{TIMEZONE_BASE_URL}?{params}"

    current_delay = 0.1  # Set the initial retry delay to 100ms.
    max_delay = 5  # Set the maximum retry delay to 5 seconds.

    while True:
        try:
            # Get the API response.
            response = urllib.request.urlopen(url)
        except urllib.error.URLError:
            pass  # Fall through to the retry loop.
        else:
            # If we didn't get an IOError then parse the result.
            result = json.load(response)

            if result["status"] == "OK":
                return result["timeZoneId"]
            elif result["status"] != "UNKNOWN_ERROR":
                # Many API errors cannot be fixed by a retry, e.g. INVALID_REQUEST or
                # ZERO_RESULTS. There is no point retrying these requests.
                raise Exception(result["error_message"])

        if current_delay > max_delay:
            raise Exception("Too many retry attempts.")

        print("Waiting", current_delay, "seconds before retrying.")

        time.sleep(current_delay)
        current_delay *= 2  # Increase the delay each time we retry.


if __name__ == "__main__":
    tz = timezone(39.6034810, -119.6822510, 1331161200)
    print(f"Timezone: {tz}")

您还应注意,应用调用链中不存在导致快速连续重复请求的重试代码。

同步请求

对 Google API 的大量同步请求可能看起来像对 Google 基础架构的分布式拒绝服务 (DDoS) 攻击,并遭到相应处理。为避免这种情况,您应确保 API 请求不会在客户端之间同步。

例如,假设某个应用显示当前时区的时间。此应用可能会在客户端操作系统中设置闹钟,使其在分钟开始时唤醒,以便更新显示的时间。在与该闹钟相关联的处理过程中,应用不应发出任何 API 调用。

针对固定的闹钟发出 API 调用是不好的做法,因为这会导致 API 调用同步到分钟开始时间(即使在不同设备之间同步),而不是在一段时间内均匀分布。设计不当的应用会在每分钟开始时产生正常水平 60 倍的流量高峰。

相比之下,一种可能的良好设计是将第二个闹钟设置为随机选择的时间。当第二个闹钟触发时,应用会调用所需的任何 API 并存储结果。当应用想要在分钟开始时更新其显示画面时,它会使用之前存储的结果,而不是再次调用 API。通过这种方法,API 调用会均匀分布。此外,当显示屏更新时,API 调用不会延迟呈现。

除了分钟开始时间,您在其他时间中不应该定位的其他常见同步时间是在一小时的开始时间和每天午夜的开始时间。

处理响应

此部分介绍如何以动态方式从 Web 服务响应中提取这些值。

Google 地图网络服务提供的响应易于理解,但不完全方便用户使用。执行查询时,您可能需要提取一些特定值,而不是显示数据。通常,您需要解析网络服务的响应并仅提取那些您感兴趣的值。

具体使用何种解析方案取决于您是以 XML 还是 JSON 格式返回输出。JSON 响应已经是 JavaScript 对象的形式,可能会在客户端上的 JavaScript 本身中进行处理。 XML 响应应使用 XML 处理器和 XML 查询语言进行处理,以处理 XML 格式内的元素。我们在以下示例中使用了 XPath,因为 XML 处理库通常支持它。

使用 XPath 处理 XML

XML 是一种相对成熟的结构化信息格式,用于数据交换。虽然 XML 不像 JSON 那样轻量级,但 XML 确实提供了更多语言支持和更强大的工具。例如,用于在 Java 中处理 XML 的代码内置于 javax.xml 软件包中。

处理 XML 响应时,您应该使用适当的查询语言在 XML 文档中选择节点,而不是假定元素位于 XML 标记内的绝对位置。XPath 是一种语言语法,用于唯一地描述 XML 文档中的节点和元素。XPath 表达式允许您标识 XML 响应文档中的特定内容。

XPath 表达式

对 XPath 有一定的了解有助于建立健全的解析方案。本部分将重点介绍如何使用 XPath 处理 XML 文档中的元素,从而解决多个元素并构建复杂查询。

XPath 使用表达式选择 XML 文档内的元素,其语法与用于目录路径的语法类似。 这些表达式可标识 XML 文档树中的元素,该树是与 DOM 类似的树。通常,XPath 表达式是 Greed 表达式,它们会匹配所有与提供的条件匹配的节点。

我们将使用以下抽象 XML 来说明我们的示例:

<WebServiceResponse>
 <status>OK</status>
 <result>
  <type>sample</type>
  <name>Sample XML</name>
  <location>
   <lat>37.4217550</lat>
   <lng>-122.0846330</lng>
  </location>
 </result>
 <result>
  <message>The secret message</message>
 </result>
</WebServiceResponse>

表达式中的节点选择

XPath 这一选择选择节点。根节点包含整个文档。您可以使用特殊表达式“/”选择此节点。请注意,根节点不是 XML 文档的顶级节点;实际上,它位于此顶级元素的上一级,包括该顶级元素。

元素节点表示 XML 文档树中的各种元素。例如,<WebServiceResponse> 元素代表我们在上述示例服务中返回的顶级元素。您可以通过绝对路径或相对路径(以是否存在前导“/”字符表示)来选择各个节点。

  • 绝对路径:“/WebServiceResponse/result”表达式选择所有属于 <WebServiceResponse> 节点的 <result> 节点。(请注意,这两个元素都来自根节点“/”。)
  • 当前上下文的相对路径:表达式“result”将与当前上下文中的任何 <result> 元素相匹配。通常,您应该不必担心上下文,因为您通常通过单个表达式处理网络服务结果。

上述任一表达式都可以通过添加由双斜杠(“//”)表示的通配符路径进行扩充。此通配符表示在中间路径中可以匹配零个或多个元素。例如,XPath 表达式“//formatted_address”将匹配当前文档中具有该名称的所有节点。表达式 //viewport//lat 将匹配可作为父项跟踪 <viewport> 的所有 <lat> 元素。

默认情况下,XPath 表达式将与所有元素相匹配。例如,您可以通过提供谓词来限定表达式,使其与方括号 ([]) 括起来。例如,XPath 表达式“/GeocodeResponse/result[2]”始终会返回第二个结果。

表达式类型
根节点
XPath 表达式:“/
选择
    <WebServiceResponse>
     <status>OK</status>
     <result>
      <type>sample</type>
      <name>Sample XML</name>
      <location>
       <lat>37.4217550</lat>
       <lng>-122.0846330</lng>
      </location>
     </result>
     <result>
      <message>The secret message</message>
     </result>
    </WebServiceResponse>
    
绝对路径
XPath 表达式:“/WebServiceResponse/result
选择
    <result>
     <type>sample</type>
     <name>Sample XML</name>
     <location>
      <lat>37.4217550</lat>
      <lng>-122.0846330</lng>
     </location>
    </result>
    <result>
     <message>The secret message</message>
    </result>
    
包含通配符的路径
XPath 表达式:“/WebServiceResponse//location
选择
    <location>
     <lat>37.4217550</lat>
     <lng>-122.0846330</lng>
    </location>
    
包含谓词的路径
XPath 表达式:“/WebServiceResponse/result[2]/message
选择
    <message>The secret message</message>
    
第一个 result 的所有直接子项
XPath 表达式:“/WebServiceResponse/result[1]/*
选择
     <type>sample</type>
     <name>Sample XML</name>
     <location>
      <lat>37.4217550</lat>
      <lng>-122.0846330</lng>
     </location>
    
resultname,其 type 文本为“sample”。
XPath 表达式:“/WebServiceResponse/result[type/text()='sample']/name
选择
    Sample XML
    

请务必注意,在选择元素时,您需要选择节点,而不仅仅是这些对象中的文本。通常,您需要遍历所有匹配的节点并提取文本。您也可以直接匹配文本节点;请参阅下面的文本节点

请注意,XPath 也支持属性节点;不过,所有 Google 地图网络服务都可以提供没有属性的元素,因此无需匹配属性。

表达式中的文本选择

XML 文档中的文本通过文本节点运算符在 XPath 表达式中指定。此运算符“text()”表示从指定的节点中提取文本。例如,XPath 表达式“//formatted_address/text()”将返回 <formatted_address> 元素内的所有文本。

表达式类型
所有文本节点(包括空格)。
XPath 表达式:“//text()
选择
    sample
    Sample XML

    37.4217550
    -122.0846330
    The secret message
    
文本选择
XPath 表达式:“/WebServiceRequest/result[2]/message/text()
选择
    The secret message
    
上下文相关选择
XPath 表达式:“/WebServiceRequest/result[type/text() = 'sample']/name/text()
选择
    Sample XML
    

或者,您也可以评估表达式并返回一组节点,然后迭代该“节点集”,从每个节点中提取文本。我们在下例中便使用了这个方法。

如需详细了解 XPath,请参阅 XPath W3C 规范

在 Java 中评估 XPath

Java 为解析 XML 以及在 javax.xml.xpath.* 软件包中使用 XPath 表达式提供了广泛支持。因此,本部分中的示例代码使用 Java 说明如何处理 XML,并解析来自 XML 服务响应的数据。

如需在 Java 代码中使用 XPath,您首先需要实例化 XPathFactory 的一个实例并在该工厂上调用 newXPath() 以创建 XPath 对象。然后,此对象可以使用 evaluate() 方法处理传递的 XML 和 XPath 表达式。

在评估 XPath 表达式时,请务必对可能返回的任何“节点集”进行迭代。由于这些结果在 Java 代码中作为 DOM 节点返回,因此您应在 NodeList 对象中捕获此类值,并迭代该对象以从这些节点中提取任何文本或值。

以下代码演示了如何创建 XPath 对象,为其分配 XML 和 XPath 表达式,并评估该表达式以输出相关内容。

import org.xml.sax.InputSource;
import org.w3c.dom.*;
import javax.xml.xpath.*;
import java.io.*;

public class SimpleParser {

  public static void main(String[] args) throws IOException {

	XPathFactory factory = XPathFactory.newInstance();

    XPath xpath = factory.newXPath();

    try {
      System.out.print("Web Service Parser 1.0\n");

      // In practice, you'd retrieve your XML via an HTTP request.
      // Here we simply access an existing file.
      File xmlFile = new File("XML_FILE");

      // The xpath evaluator requires the XML be in the format of an InputSource
	  InputSource inputXml = new InputSource(new FileInputStream(xmlFile));

      // Because the evaluator may return multiple entries, we specify that the expression
      // return a NODESET and place the result in a NodeList.
      NodeList nodes = (NodeList) xpath.evaluate("XPATH_EXPRESSION", inputXml, XPathConstants.NODESET);

      // We can then iterate over the NodeList and extract the content via getTextContent().
      // NOTE: this will only return text for element nodes at the returned context.
      for (int i = 0, n = nodes.getLength(); i < n; i++) {
        String nodeString = nodes.item(i).getTextContent();
        System.out.print(nodeString);
        System.out.print("\n");
      }
    } catch (XPathExpressionException ex) {
	  System.out.print("XPath Error");
    } catch (FileNotFoundException ex) {
      System.out.print("File Error");
    }
  }
}