Distance Matrix API ウェブサービスを使用する際のベスト プラクティス

Google Maps Platform ウェブサービスは、Google サービスへの HTTP インターフェースの集合であり、地図アプリケーションに地理データを提供します。

このガイドでは、ウェブサービス リクエストの設定とサービス レスポンスの処理に役立つ一般的な方法について説明します。Distance Matrix API について詳しくは、デベロッパー ガイドをご覧ください。

ウェブサービスとは

Google Maps Platform ウェブサービスは、外部サービスに Maps API データをリクエストしたり、そのデータをマップ アプリケーション内で使用したりするためのインターフェースです。これらのサービスは、Google Maps Platform 利用規約のライセンス制限に基づき、地図と組み合わせて使用するように設計されています。

Maps API ウェブサービスは、特定の URL に対する HTTP(S) リクエストを使用し、URL パラメータや JSON 形式の POST データを引数としてサービスに渡します。通常、これらのサービスはレスポンスの本文でデータを JSON または XML として返し、アプリケーションによる解析や処理を行います。

一般的に、Distance Matrix API リクエストの形式は次のようになります。

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

ここで、output はレスポンスの形式(通常は json または xml)を示します。

: Distance Matrix API のすべてのアプリケーションで認証が必要です。認証情報の詳細を確認する。

SSL/TLS アクセス

API キーを使用する、またはユーザーデータが含まれるすべての Google Maps Platform リクエストには、HTTPS が必須です。機密データを含む HTTP 経由で送信されたリクエストは拒否される場合があります。

有効な URL の作成

「有効」な URL とは何か、説明の必要はないと考えられるかもしれませんが、それほど単純なことではありません。ブラウザのアドレスバーに入力される URL には特殊文字("上海+中國" など)が含まれている場合があります。このような特殊文字は、ブラウザで別のエンコードに内部的に変換してから送信する必要があります。同様に、UTF-8 入力を生成または受け付けるコードでは、UTF-8 の文字が使用された URL を「有効」な URL として扱うことがありますが、それらの文字はウェブサーバーに送信する前に変換する必要があります。このプロセスは、URL エンコードまたはパーセント エンコードと呼ばれます。

特殊文字

すべての URL は URI(Uniform Resource Identifier)仕様で規定されている構文に従う必要があるため、特殊文字を変換する必要があります。つまり、URL には、ASCII 文字の特別なサブセット(よく使用される英数記号および URL 内で制御文字として使用される予約文字)のみを含める必要があります。次の表は、こうした特殊記号をまとめたものです。

有効な URL 文字の概要
セット文字URL での使用法
英数字 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)など
未予約 - _ . ~ テキスト文字列
予約済み ! * ' ( ) ; : @ & = + $ , / ? % # [ ] 制御文字やテキスト文字列

有効な URL を作成するときは、表に示されている文字のみを含める必要があります。しかし、URL での使用がこの文字セットだけに制限された場合、通常は 2 つの問題が発生します。1 つは省略、もう 1 つは置き換えです。

  • 処理する文字が上記のセットに含まれない場合。たとえば、「上海+中國」のような英語以外の文字は、上記の文字を使用してエンコードする必要があります。一般的な命名規則では、URL 内で使用できないスペースもプラス記号 '+' を使用して表します。
  • 上記のセットに予約文字として含まれる文字を、リテラル文字として使用する必要がある場合。たとえば、「?」は URL 内でクエリ文字列の先頭を示すために使用されます。文字列「? and the Mysterions」を使用する場合は、文字 '?' をエンコードする必要があります。

URL エンコードが必要なすべての文字を、'%' と、UTF-8 文字に対応する 2 文字の 16 進数値を使用してエンコードします。たとえば、UTF-8 の「上海+中國」は、「%E4%B8%8A%E6%B5%B7%2B%E4%B8%AD%E5%9C%8B」として URL エンコードされます。文字列「? and the Mysterians」は、「%3F+and+the+Mysterians」または「%3F%20and%20the%20Mysterians」として URL エンコードされます。

エンコードが必要な一般的な文字

エンコードする必要がある一般的な文字は次のとおりです。

危険な文字 エンコードされた値
Space %20
%22
< %3C
> %3E
# %23
% %25
| %7C

ユーザー入力から受け取った URL の変換には、場合によって注意が必要です。たとえば、ユーザーが住所を「5th&Main St.」と入力することも考えられます。通常は、ユーザー入力をリテラル文字として処理して、URL をパーツから作成する必要があります。

さらに、URL は、すべての Google Maps Platform ウェブサービスと Static Web API で 16,384 文字に制限されています。ほとんどのサービスでは、この文字制限に達することはめったにありません。ただし、複数のパラメータを持つ特定のサービスでは、URL が長くなる可能性があります。

Google API の適切な使用

API クライアントの設計に問題があると、インターネットと Google のサーバーの両方に必要以上の負荷がかかる可能性があります。このセクションでは、API のクライアントのベスト プラクティスについて説明します。ここで紹介するベスト プラクティスに従うことで、意図せず API の不正使用によってアプリがブロックされるのを防ぐことができます。

指数関数的バックオフ

まれに、リクエストの処理中になんらかの問題が発生し、4XX または 5XX HTTP レスポンス コードが返されることがあります。また、クライアントと Google のサーバーの間で単に TCP 接続が失敗することもあります。最初のリクエストが失敗した場合にフォローアップ リクエストが成功することがあるため、多くの場合はリクエストを再試行する価値があります。ただし、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 リクエストが同期されないようにする必要があります。

たとえば、現在のタイムゾーンで時刻を表示するアプリケーションについて考えてみましょう。このアプリケーションは通常、クライアント オペレーティング システムで 1 分毎に起動するアラームを設定し、表示される時刻を更新できるようにします。アプリケーションは、アラームに関連する処理の一環として API 呼び出しを実行しないでください。

固定アラームに応答して API 呼び出しを行うと、API 呼び出しが時間とともに均等に分散されるのではなく、異なるデバイス間でも 1 分ごとに同期されるため、適切ではありません。アプリケーションの設計が不適切で、このような処理を行うと、毎分 1 分ごとに通常の 60 倍のレベルでトラフィックが急増します。

その代わりに、ランダムに選択された時間に 2 つ目のアラームを設定することをおすすめします。この 2 番目のアラームを起動すると、アプリケーションは必要な API を呼び出し、結果を保存します。アプリケーションで 1 分の開始時に表示を更新する場合は、API を再度呼び出すのではなく、以前に保存された結果を使用します。このアプローチでは、API 呼び出しが時間の経過とともに均等に分散されます。さらに、ディスプレイの更新中に API 呼び出しによってレンダリングが遅延することはありません。

毎時 0 分を除けば、他の一般的な同期時刻の対象にしないで注意する必要があります。

レスポンスの処理

このセクションでは、これらの値をウェブサービス レスポンスから動的に抽出する方法について説明します。

Google マップのウェブサービスが提供するレスポンスは、わかりやすいものですが、ユーザー フレンドリーとは言えません。クエリを実行するときは、データセットを表示するのではなく、いくつかの特定の値を抽出する必要があります。通常は、ウェブサービスからのレスポンスを解析し、必要な値のみを抽出します。

使用する解析スキームは、出力を XML と JSON のどちらで返すかによって異なります。JSON レスポンスは、すでに JavaScript オブジェクトの形式で処理されているため、クライアントの JavaScript 内で処理できます。XML レスポンスは、XML プロセッサと XML クエリ言語を使用して処理し、XML 形式の要素をアドレス指定します。次の例では、XML 処理ライブラリで XPath がサポートされているため、XPath を使用しています。

XPath による XML の処理

XML は、データ交換に使用される比較的成熟した構造化情報形式です。XML は JSON ほど軽量ではありませんが、より多くの言語サポートとより堅牢なツールを提供します。たとえば、Java で XML を処理するコードは javax.xml パッケージに組み込まれています。

XML レスポンスを処理するときは、要素が XML マークアップ内の絶対位置にあると想定するのではなく、XML ドキュメント内のノードの選択に適切なクエリ言語を使用する必要があります。XPath は、XML ドキュメント内のノードと要素を一意に記述するための言語構文です。XPath 式を使用すると、XML レスポンス ドキュメント内の特定のコンテンツを識別できます。

XPath 式

XPath についてある程度の知識があると、堅牢な解析スキームの開発に大いに役立ちます。このセクションでは、XML ドキュメント内の要素を XPath でどのように処理するかに焦点を当てます。これにより、複数の要素に対処して複雑なクエリを作成できます。

XPath では、ディレクトリ パスに使用されている構文と同様の構文で、式を使用して XML ドキュメント内の要素を選択します。これらの式は、DOM のツリーに似た階層ツリーである XML ドキュメント ツリー内の要素を識別します。通常、XPath 式は欲張り(greedy)であり、指定された条件に一致するすべてのノードに一致することを示します。

ここでは、例を示すために次の抽象 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 ドキュメントの最上位ノードではありません。実際には、この最上位要素の 1 レベル上に存在し、その要素が含まれています。

要素ノードは、XML ドキュメント ツリー内のさまざまな要素を表します。たとえば、<WebServiceResponse> 要素は、上記のサンプル サービスで返される最上位要素を表します。個々のノードを選択する場合は、先頭に「/」を付けるかどうかで絶対パスまたは相対パスを指定します。

  • 絶対パス: 「/WebServiceResponse/result」式は、<WebServiceResponse> ノードの子であるすべての <result> ノードを選択します。(これらの要素は両方ともルートノード「/」の子孫です)。
  • 現在のコンテキストからの相対パス: 式「result」は、現在のコンテキスト内の任意の <result> 要素に一致します。通常は 1 つの式でウェブサービスの結果を処理するため、コンテキストを気にする必要はありません。

どちらの式も、二重スラッシュ(「//」)で示されるワイルドカード パスを追加することで拡張できます。このワイルドカードは、介在するパスで 0 個以上の要素が一致する可能性があることを示します。たとえば、XPath 式「//formatted_address」は、現在のドキュメント内でその名前を持つすべてのノードに一致します。式 //viewport//lat は、<viewport> を親として追跡できるすべての <lat> 要素と一致します。

デフォルトでは、XPath 式はすべての要素に一致します。特定の要素と一致するように式を制限するには、角かっこ([])で囲まれたpredicateを指定します。たとえば、XPath 式「/GeocodeResponse/result[2]」は常に 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>
    
type テキストが「sample」である resultname
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 は、javax.xml.xpath.* パッケージ内の XML の解析と 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");
    }
  }
}