שיטות מומלצות לשימוש בשירותי האינטרנט של Geocoding API

שירותי האינטרנט של הפלטפורמה של מפות Google הם אוסף של ממשקי HTTP לשירותי Google שמספקים נתונים גיאוגרפיים לאפליקציות של המפות שלכם.

במדריך הזה מתוארות כמה שיטות נפוצות שמועילות להגדרת בקשות של שירותי אינטרנט ולעיבוד תשובות לשירותים. לקבלת המסמכים המלאים של Geocoding API, עיינו במדריך למפתחים.

מהו שירות אינטרנט?

שירותי האינטרנט של הפלטפורמה של מפות Google הם ממשק לבקשת נתונים של ממשק ה-API של מפות Google משירותים חיצוניים ולשימוש בנתונים מתוך אפליקציות מפות Google שלכם. השימוש בשירותים האלה בשילוב עם מפה, בהתאם להגבלות הרישיון בתנאים ובהגבלות של הפלטפורמה של מפות Google.

שירותי האינטרנט של ממשקי ה-API של מפות Google משתמשים בבקשות HTTP(S) לכתובות URL ספציפיות, ומעבירים פרמטרים של כתובות אתרים או נתוני POST בפורמט JSON כארגומנטים לשירותים. באופן כללי, השירותים האלו מחזירים נתונים בגוף התשובה בתור JSON או XML לצורך ניתוח ו/או עיבוד על ידי האפליקציה.

בדרך כלל, בקשה אופיינית ל-Geocoding API היא באופן הבא:

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

כאשר output מציין את פורמט התשובה (בדרך כלל json או xml).

הערה: כל האפליקציות של Geocoding API מחייבות אימות. מידע נוסף על פרטי כניסה לאימות

גישת SSL/TLS

חובה להשתמש ב-HTTPS לכל הבקשות של הפלטפורמה של מפות Google שמשתמשות במפתחות API או שמכילות נתוני משתמשים. בקשות שמבוצעות באמצעות HTTP ומכילות מידע אישי רגיש עשויות להידחות.

יצירת כתובת URL חוקית

יכול להיות שתחשבו שכתובת URL 'תקפה' מובנת מאליה, אבל זה לא המצב. לדוגמה, כתובת URL שהוזנה בסרגל הכתובות בדפדפן עשויה להכיל תווים מיוחדים (למשל, "上海+中國"). לפני ההעברה, הדפדפן צריך לתרגם את התווים האלה לקידוד שונה. באותו אסימון, כל קוד שיוצר או מקבל קלט UTF-8 עלול להתייחס לכתובות URL עם תווי UTF-8 כ'תקינים', אבל הוא יצטרך לתרגם את התווים האלה לפני שהם שולחים אותם לשרת אינטרנט. התהליך הזה נקרא קידוד כתובות URL או קידוד באחוזים.

תווים מיוחדים

אנחנו צריכים לתרגם תווים מיוחדים כי כל כתובות ה-URL צריכות להתאים לתחביר שצוין במפרט Uniform Resource Identifier (URI). למעשה, כתובות 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 X Y Z 0 1 2 3 מחרוזות טקסט, שימוש בסכמה (http), יציאה (8080) וכו'.
לא שמור - _ . ~ מחרוזות טקסט
בוצעה הזמנה ! * ' ( ) ; : @ & = + $ , / ? % # [ ] תווי בקרה ו/או מחרוזות טקסט

כשיוצרים כתובת URL תקינה, צריך לוודא שהיא מכילה רק את התווים שמוצגים בטבלה. שינוי של כתובת URL לשימוש בקבוצת התווים הזו מוביל בדרך כלל לשתי בעיות, אחת להשמטת נתונים והחלפה:

  • התווים שבהם ברצונך לטפל קיימים מחוץ לקבוצה שלמעלה. לדוגמה, תווים בשפות זרות כמו 上海+中國 צריכים להיות מקודדים באמצעות התווים שלמעלה. לפי המוסכמה הפופולרית, רווחים (שלא מותרים בכתובות URL) מיוצגים לעיתים קרובות גם באמצעות תו '+'.
  • התווים קיימים במסגרת הקבוצה כתווים שמורים, אבל צריך להשתמש בהם באופן מילולי. לדוגמה, הפונקציה ? משמשת בכתובות URL כדי לציין את ההתחלה של מחרוזת השאילתה. אם רוצים להשתמש במחרוזת '?' וב-Mysterions', צריך לקודד את התו '?'.

כל התווים לקידודי התווים שמתאימים לכתובות URL מקודדים באמצעות תו '%' וערך הקסדצימלי בן שני תווים שתואם לתו UTF-8. לדוגמה, 上海+中國 בקידוד UTF-8 יותאם לקידודי התווים שמתאימים לכתובות URL באופן הבא: %E4%B8%8A%E6%B5%B7%2B%E4%B8%AD%E5%9C%8B. המחרוזת ? and the Mysterians תהיה בקידוד כתובת ה-URL כ-%3F+and+the+Mysterians או כ-%3F%20and%20the%20Mysterians.

תווים נפוצים שדורשים קידוד

הנה מספר תווים נפוצים שחובה לקודד:

תו לא בטוח ערך מקודד
מרחב %20
" %22
< %3C
> %3E
# %23
% %25
| %7C

לפעמים קשה להמיר כתובת URL שמקבלים מפרטים של משתמשים. לדוגמה, משתמש יכול להזין כתובת בפורמט "רחוב חמישי וראשון" באופן כללי, צריך ליצור את כתובת ה-URL מהחלקים שלה ולהתייחס לכל קלט של משתמשים כתווים מילוליים.

בנוסף, כתובות ה-URL מוגבלות ל-16,384 תווים לכל שירותי האינטרנט וממשקי ה-API הסטטיים באינטרנט של הפלטפורמה של מפות Google. ברוב השירותים שלרוב תתקרבו למגבלת התווים הזו. עם זאת, חשוב לשים לב שלשירותים מסוימים יש מספר פרמטרים שעשויים להוביל לכתובות URL ארוכות.

שימוש מנומס בממשקי API של Google

לקוחות API שלא מעוצבים כראוי יכולים ליצור עומס גדול יותר מהנדרש גם באינטרנט וגם בשרתים של Google. הקטע הזה כולל כמה שיטות מומלצות ללקוחות של ממשקי ה-API. כדאי לפעול לפי השיטות המומלצות האלה כדי למנוע את חסימת האפליקציה עקב ניצול לרעה של ממשקי ה-API בטעות.

ניהול שגיאות וניסיונות חוזרים

לקבלת מידע על קודי התגובה UNKNOWN_ERROR או OVER_QUERY_LIMIT מ-Geocoding API, כדאי לקרוא על ניהול שגיאות וניסיונות חוזרים.

השהיה מעריכית לפני ניסיון חוזר (exponential backoff)

במקרים נדירים, יכול להיות שמשהו ישתבש בזמן מילוי הבקשה: יכול להיות שתקבלו קוד תגובה 4XX או 5XX של HTTP, או שחיבור ה-TCP פשוט ייכשל במקום כלשהו בין הלקוח לשרת של Google. לעיתים קרובות משתלם לנסות שוב את הבקשה, כי בקשת ההמשך עשויה להצליח כאשר הבקשה המקורית נכשלה. עם זאת, חשוב לא ליצור בקשות חוזרות ונשנות לשרתים של Google. הלולאה הזו עלולה לגרום לעומס יתר על הרשת בין הלקוח ל-Google, ולגרום לבעיות לגורמים רבים.

עדיף לנסות שוב ושוב עם יותר עיכובים בין הניסיונות. בדרך כלל ההשהיה גדלת בגורם מכפיל בכל ניסיון, גישה שנקראת השהיה מעריכית לפני ניסיון חוזר (exponential backoff).

לדוגמה, נניח שיש לכם אפליקציה שרוצה לשלוח את הבקשה הזו ל-Time Zone API:

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

הדוגמה הבאה ב-Python מראה איך לשלוח את הבקשה עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff):

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

כמו כן, כדאי לוודא שאין ניסיון חוזר של קוד גבוה יותר בשרשרת הקריאה של האפליקציה, שמובילה לבקשות חוזרות ברצף מהיר.

בקשות מסונכרנות

מספר גדול של בקשות מסונכרנות לממשקי ה-API של Google יכולות להיראות כמו התקפת מניעת שירות מבוזרת (DDoS) בתשתית של Google, וצריך לטפל בהן בהתאם. כדי למנוע זאת, צריך לוודא שבקשות ה-API לא מסונכרנות בין הלקוחות.

לדוגמה, נבחן אפליקציה שמציגה את השעה באזור הזמן הנוכחי. האפליקציה כנראה תגדיר התראה במערכת ההפעלה של הלקוח שמוציאה אותה ממצב שינה בתחילת הדקה, כדי שיהיה אפשר לעדכן את השעה המוצגת. האפליקציה לא צריכה לבצע קריאות ל-API כחלק מהעיבוד שמשויך להתראה הזו.

אי אפשר לבצע קריאות ל-API בתגובה להתראה קבועה, כי הקריאות ל-API יסתנכרנו לתחילת הדקות, גם בין מכשירים שונים, ולא לפזר אותן באופן שווה לאורך זמן. אפליקציה שתוכננה בצורה לא נכונה תייצר עלייה חדה בתנועת גולשים פי 60 מהרמות הרגילות בתחילת כל דקה.

במקום זאת, אפשרות טובה אחת היא להגדיר התראה שנייה למועד שנבחר באופן אקראי. כשההתראה השנייה מופעלת, האפליקציה קוראת לממשקי ה-API שהיא צריכה ושומרת את התוצאות. כשהאפליקציה מבקשת לעדכן את התצוגה שלה בתחילת דקה, היא משתמשת בתוצאות שנשמרו בעבר במקום לקרוא שוב ל-API. בגישה הזו, הקריאות ל-API מחולקות באופן שווה לאורך הזמן. בנוסף, הקריאות ל-API לא מעכבות את הרינדור במהלך עדכון המסך.

מלבד תחילת הדקה, זמני סנכרון נפוצים אחרים שבהם כדאי להיזהר לא לטרגט הם בתחילת השעה, בתחילת כל יום בחצות.

מתבצע עיבוד של התשובות

בקטע הזה מוסבר איך לחלץ את הערכים האלה באופן דינמי מתשובות של שירותי אינטרנט.

שירותי האינטרנט של מפות Google מספקים תשובות קלות להבנה, אבל לא בדיוק ידידותיות למשתמש. כשמריצים שאילתה, במקום להציג קבוצת נתונים, עדיף לחלץ כמה ערכים ספציפיים. באופן כללי, רצוי לנתח תשובות משירות האינטרנט ולחלץ רק את הערכים שמעניינים אתכם.

סכימת הניתוח שבה משתמשים תלויה בשאלה אם מחזירים פלט ב-XML או ב-JSON. תגובות JSON, שהן כבר אובייקטים של JavaScript, יכולות לעבור עיבוד בתוך JavaScript עצמו אצל הלקוח. כדי לטפל ברכיבים בפורמט XML, צריך לעבד תגובות XML באמצעות מעבד XML ושפת שאילתות XML. בדוגמאות הבאות נשתמש ב-XPath, כי בדרך כלל אפשר להשתמש בו בספריות עיבוד XML.

עיבוד XML באמצעות XPath

XML הוא פורמט מידע מובנה בוגר יחסית שמשמש להחלפת נתונים. למרות שהוא לא קל כמו JSON, אבל XML מספק יותר תמיכה בשפה וכלים חזקים יותר. למשל, קוד לעיבוד XML ב-Java מובנה בחבילות 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 בוחרות צמתים. הצומת ברמה הבסיסית (root) כולל את כל המסמך. הצומת הזה נבחר באמצעות הביטוי המיוחד /. שימו לב שצומת הרמה הבסיסית (root) הוא לא הצומת ברמה העליונה של מסמך ה-XML, אלא הוא נמצא ברמה אחת מעל הרכיב ברמה העליונה וכולל אותו.

צמתים של רכיבים מייצגים את הרכיבים השונים בעץ מסמכי ה-XML. למשל, רכיב <WebServiceResponse> מייצג את הרכיב ברמה העליונה שמוחזר בשירות לדוגמה שלמעלה. אפשר לבחור צמתים נפרדים באמצעות נתיבים מוחלטים או נתיבים יחסיים, כאשר תו '/' מוביל או חסר.

  • נתיב מוחלט: הביטוי "/WebServiceResponse/result" בוחר את כל הצמתים <result> שהם צאצאים של הצומת <WebServiceResponse>. (שימו לב ששני הרכיבים האלו מגיעים מצומת הרמה הבסיסית (root) "/".)
  • נתיב יחסי מההקשר הנוכחי: הביטוי "result" יתאים לכל רכיב של <result> בהקשר הנוכחי. באופן כללי, לא צריך לדאוג לגבי ההקשר, כי בדרך כלל מעבדים תוצאות של שירותי אינטרנט באמצעות ביטוי יחיד.

אפשר להרחיב כל אחד מהביטויים האלה על-ידי הוספה של נתיב עם תו כללי לחיפוש, מסומן בלוכסן כפול ('//'). התו הכללי לחיפוש הזה מציין שיכול להיות שאפס רכיבים או יותר בנתיב ביניהם תואמים. לדוגמה, ביטוי ה-XPath '//formatted_address' יתאים לכל הצמתים של השם הזה במסמך הנוכחי. הביטוי //viewport//lat יתאים לכל הרכיבים של <lat> שיכולים לעקוב אחרי <viewport> כהורה.

כברירת מחדל, ביטויי XPath תואמים לכל הרכיבים. אפשר להגביל את הביטוי כך שיתאים לרכיב מסוים באמצעות הוספת predicate, שמוקף בסוגריים מרובעים ([]). ביטוי ה-XPath '/GeocodeResponse/result[2]' תמיד מחזיר את התוצאה השנייה, לדוגמה.

סוג הביטוי
צומת בסיס
XPath Expression: '/'
בחירה:
    <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 Expression: '/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 Expression: '/WebServiceResponse//location'
בחירה:
    <location>
     <lat>37.4217550</lat>
     <lng>-122.0846330</lng>
    </location>
    
נתיב עם פרדיקט
XPath Expression: '/WebServiceResponse/result[2]/message'
בחירה:
    <message>The secret message</message>
    
כל הצאצאים הישירים של result הראשונים
XPath Expression: '/WebServiceResponse/result[1]/*'
בחירה:
     <type>sample</type>
     <name>Sample XML</name>
     <location>
      <lat>37.4217550</lat>
      <lng>-122.0846330</lng>
     </location>
    
name של result שהטקסט ב-type שלו הוא "sample".
XPath Expression: '/WebServiceResponse/result[type/text()='sample']/name'
בחירה:
    Sample XML
    

חשוב לציין שכאשר בוחרים רכיבים, בוחרים צמתים ולא רק את הטקסט שבאובייקטים האלה. באופן כללי, רצוי לבצע איטרציה לכל הצמתים התואמים ולחלץ את הטקסט. אפשר גם להתאים ישירות לצמתים של טקסט. מידע נוסף מופיע בקטע צומתי טקסט שבהמשך.

שימו לב ש-XPath תומך גם בצמתים של מאפיינים. עם זאת, כל שירותי האינטרנט של מפות Google מציגים רכיבים ללא מאפיינים, ולכן לא נדרשת התאמה של מאפיינים.

בחירת טקסט בהבעות

טקסט במסמך XML מצוין בביטויי XPath באמצעות אופרטור צומת טקסט. האופרטור 'text()' מציין חילוץ טקסט מהצומת שצוין. לדוגמה, ביטוי ה-XPath '//formatted_address/text()' יחזיר את כל הטקסט בתוך רכיבי <formatted_address>.

סוג הביטוי
כל צומתי הטקסט (כולל רווחים לבנים)
XPath Expression: '//text()'
בחירה:
    sample
    Sample XML

    37.4217550
    -122.0846330
    The secret message
    
בחירת טקסט
XPath Expression: '/WebServiceRequest/result[2]/message/text()'
בחירה:
    The secret message
    
בחירה רגישה להקשר
XPath Expression: '/WebServiceRequest/result[type/text() = 'sample']/name/text()'
בחירה:
    Sample XML
    

לחלופין, אפשר להעריך ביטוי ולהחזיר קבוצת צמתים, ואז לחזור על אותה "קבוצת צמתים" ולחלץ את הטקסט מכל צומת. אנחנו משתמשים בגישה הזו בדוגמה הבאה.

מידע נוסף על XPath זמין במפרט XPath W3C.

הערכת XPath ב-Java

ב-Java יש תמיכה רחבה בניתוח XML ובשימוש בביטויי XPath בתוך החבילה javax.xml.xpath.*. לכן הקוד לדוגמה בקטע הזה משתמש ב-Java כדי להדגים איך לטפל ב-XML ולנתח נתונים מתשובות לשירות XML.

כדי להשתמש ב-XPath בקוד Java שלכם, קודם צריך ליצור מכונה של XPathFactory ולקרוא ל-newXPath() במפעל הזה כדי ליצור אובייקט XPath . לאחר מכן האובייקט יכול לעבד ביטויי XML ו-XPath שהועברו באמצעות השיטה evaluate().

כשבוחנים ביטויי XPath, חשוב לבצע איטרציה של כל 'קבוצות צמתים' אפשריות שיוחזרו. התוצאות האלה מוחזרות כצומתי DOM בקוד Java, ולכן צריך לתעד מספר ערכים כאלה בתוך אובייקט 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");
    }
  }
}