使用 Time Zone API 網路服務的最佳做法

Google 地圖平台網路服務是 Google 服務的 HTTP 介面集合,可為地圖應用程式提供地理資料。

本指南將說明一些常見做法,協助您設定網路服務要求和處理服務回應。如需 Time Zone API 的完整說明文件,請參閱開發人員指南

什麼是網路服務?

Google 地圖平台網路服務是一種介面,可用於向外部服務要求 Maps API 資料,並在 Google 地圖應用程式中使用這些資料。根據《Google 地圖平台服務條款》的「授權限制」規定,這些服務的設計目的是與地圖搭配使用。

Maps API 網路服務會使用 HTTP(S) 要求存取特定網址,並將網址參數和/或 JSON 格式的 POST 資料做為引數傳遞給服務。一般來說,這些服務會以 JSON 或 XML 格式在回應主體中傳回資料,供應用程式剖析和/或處理。

一般 Time Zone API 要求的格式如下:

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

其中 output 表示回應格式 (通常為 jsonxml)。

注意:所有 Time Zone API 應用程式都需要驗證。進一步瞭解驗證憑證

SSL/TLS 存取權

凡是使用 API 金鑰或包含使用者資料的 Google 地圖平台要求,都必須透過 HTTPS 傳送。透過 HTTP 提出的含有機密資料要求可能會遭到拒絕。

建立有效網址

您可能認為網址是否「有效」一眼就能判斷,但實際情況不然。例如,在瀏覽器的網址列內輸入的網址可能包含特殊字元 (例如 "上海+中國");瀏覽器必須在內部將這些字元轉譯為其他編碼方式才能傳送。同理可證,產生或接受 UTF-8 輸入值的任何程式碼都可能會將含有 UTF-8 字元的網址視為「有效網址」,但也需要先轉譯這些字元,才能向外傳送至網路伺服器。這個過程稱為網址編碼百分比編碼

特殊字元

所有網址都必須符合統一資源 ID (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 地圖平台網路服務和 Static Web API 的網址長度上限都是 16, 384 個字元。對於大部分的服務而言,很少出現接近此字元限制的情況。但請注意,某些服務的幾個參數可能會產生較長的網址。

禮貌使用 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 呼叫與分鐘同步,甚至在不同裝置之間同步,而非在一段時間內平均分配。設計不良的應用程式會在每分鐘的開始時,產生比正常值高出六十倍的流量尖峰。

相反地,一個可能的良好設計是將第二個鬧鐘設為隨機選擇的時間。當第二個鬧鐘觸發時,應用程式會呼叫所需的任何 API,並儲存結果。當應用程式要在分鐘開始時更新顯示內容時,會使用先前儲存的結果,而不是再次呼叫 API。使用這種方法時,API 呼叫會在一段時間內平均分散。此外,在更新顯示畫面時,API 呼叫不會延遲轉譯。

除了分鐘開始時間之外,您也應避免在小時開始時間和每天午夜開始時間同步處理。

處理回應

本章節討論如何從網路服務回應中,以動態方式擷取這些值。

Google 地圖網路服務提供的回應雖然容易理解,但不一定符合使用者需求。執行查詢時,您可能會想擷取幾個特定值,而不是顯示一組資料。一般來說,您會想要剖析網路服務的回應,並只擷取您感興趣的值。

您使用的剖析配置取決於您要以 XML 還是 JSON 格式傳回輸出內容。JSON 回應已採用 JavaScript 物件的形式,因此可在用戶端的 JavaScript 中處理。您應使用 XML 處理器和 XML 查詢語言處理 XML 回應,以便處理 XML 格式中的元素。我們在以下範例中使用 XPath,因為 XML 處理程式庫通常支援這項功能。

利用 XPath 處理 XML

XML 是用於資料交換的相對成熟的結構化資訊格式。雖然 XML 不像 JSON 那麼輕巧,但它確實提供更多語言支援和更強大的工具。舉例來說,用於在 Java 中處理 XML 的程式碼會建構在 javax.xml 套件中。

處理 XML 回應時,您應使用適當的查詢語言,在 XML 文件中選取節點,而非假設元素位於 XML 標記中的絕對位置。XPath 是一種語法,可用於描述 XML 文件中的節點和元素。您可以使用 XPath 運算式,在 XML 回應文件中識別特定內容。

XPath 運算式

熟悉 XPath 有助於開發可靠的剖析方案。本節將著重於說明如何使用 XPath 處理 XML 文件中的元素,讓您能夠處理多個元素並建立複雜的查詢。

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 文件的頂層節點;實際上,它位於這個頂層元素上一層,並包含這個元素。

元素節點代表 XML 文件樹狀結構中的各種元素。舉例來說,<WebServiceResponse> 元素代表上述範例服務中傳回的頂層元素。您可以透過絕對或相對路徑選取個別節點,這可從開頭是否有「/」字元來判斷。

  • 絕對路徑:"/WebServiceResponse/result" 運算式會選取所有 <result> 節點,這些節點是 <WebServiceResponse> 節點的子項。(請注意,這兩個元素都是從根節點「/」衍生而來)。
  • 從目前情境的相對路徑:運算式「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 地圖網路服務都會提供不含屬性的元素,因此不需要比對屬性。

運算式中的文字選項

透過 text node 運算子,在 XPath 運算式中指定 XML 文件中的文字。這個運算子「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");
    }
  }
}