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

Google Maps Platform ウェブサービスは、地図アプリケーションに地理データを提供する Google サービスへの HTTP インターフェースのコレクションです。

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

ウェブサービスとは

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

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

一般的な Geocoding API リクエストの形式は次のとおりです。

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

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

注: すべての Geocoding 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 エンコードされます。

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

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

危険な文字 エンコードされた値
スペース %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 の不注意な不正使用によりアプリケーションがブロックされるのを回避できます。

エラーの対処と再試行

Geocoding API からの UNKNOWN_ERROR または OVER_QUERY_LIMIT レスポンス コードについては、エラーと再試行の管理をご覧ください。

指数バックオフ

まれに、リクエストの処理中にエラーが発生することがあります。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 呼び出しが時間の経過とともに均等に分散されるのではなく、異なるデバイス間でも分の開始に同期されるため、適切ではありません。不適切に設計されたアプリケーションでは、毎分の開始時に通常の 60 倍のトラフィックの急増が発生します。

代わりに、2 つ目のアラームをランダムに選択した時刻に設定するという設計が考えられます。この 2 番目のアラームがトリガーされると、アプリは必要な API を呼び出して結果を保存します。アプリが 1 分の開始時に表示を更新する場合は、API を再度呼び出すのではなく、以前に保存した結果を使用します。この方法では、API 呼び出しが時間の経過とともに均等に分散されます。さらに、API 呼び出しによって、ディスプレイの更新時にレンダリングが遅れることはありません。

分の開始以外に、ターゲットにしないよう注意する必要がある一般的な同期時間は、時間の開始と、毎日の深夜 0 時の開始です。

レスポンスの処理

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

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

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

XPath による XML の処理

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

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

XPath 式

XPath にある程度精通していると、堅牢な解析スキームを開発するのに役立ちます。このセクションでは、XML ドキュメント内の要素を XPath でアドレス指定する方法について説明します。これにより、複数の要素をアドレス指定して複雑なクエリを作成できます。

XPath は、ディレクトリ パスに使用される構文と同様の構文を使用して、式を使用して XML ドキュメント内の要素を選択します。これらの式は、XML ドキュメント ツリー内の要素を識別します。これは、DOM のツリーに似た階層ツリーです。一般に、XPath 式は最長一致の原則があるため、指定された条件に一致するすべてのノードが一致します。

以下の抽象 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> 要素に一致します。通常、ウェブサービスの結果は単一の式で処理するため、コンテキストについて心配する必要はありません。

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

デフォルトでは、XPath 式はすべての要素に一致します。述語を角かっこ([])で囲んで指定することで、特定の要素に一致するように式を制限できます。たとえば、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」である result の name。
XPath 式:  "/WebServiceResponse/result[type/text()='sample']/name"
選択:
    Sample XML
    

要素を選択するときは、オブジェクト内のテキストだけでなく、ノードを選択することが重要です。通常は、一致するすべてのノードを反復処理してテキストを抽出します。テキストノードを直接照合することもできます。下記のテキストノード をご覧ください。

XPath は属性ノードもサポートしていますが、Google Maps ウェブサービスはすべて属性のない要素を提供するため、属性の一致は必要ありません。

式でのテキスト選択

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");
    }
  }
}