デルタ システム ソリューションズ 株式会社

Google Maps APIでお店のランキング表示をしてみると[PHP & GAS編](3/3)

GASでお店ランキング表示

今度は、サーバサイド・スクリプトの注目株「GAS」で、Google Maps APIを利用してお店ランキング表示を実装します。

「GAS」(Google Apps Script)は、その名の通り、Googleが提供するサーバサイド・スクリプトで、JavaScriptをベースに作られているので非常に取っ付きやすい言語です。
しかも、自分でレンタルサーバを借りなくても、Googleアカウントを持っていれば無料でGoogleサーバを利用できるので、お手軽に開発できるのがうれしいですね!


【サンプル2】GASでお店ランキング表示

/*
 HTTP GETをハンドリング
*/
function doGet(e){
  //検索場所
  var address = e.parameter.address;
  //検索キーワード
  var keyword = e.parameter.keyword;
  //検索範囲(メートル)
  var radius = e.parameter.radius;

  //お店情報取得
  var resultHTML = getPlace(address, keyword, radius);
  //HTMLを返却
  var output = HtmlService.createHtmlOutput(resultHTML);
  output.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
  return output;
}

/*
 お店情報取得
  address:検索場所
  keyword:検索キーワード
  radius:検索範囲(メートル)
*/
function getPlace(address, keyword, radius) {
  //Google Maps API の APIキー
  var apiKey = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
  
  //【Geocoding API】住所→緯度・経度
  var geocodeApiUrl = "https://maps.googleapis.com/maps/api/geocode/json";
  geocodeApiUrl += "?key=" + apiKey;
  geocodeApiUrl += "&address=" + encodeURIComponent(address);
  
  //Geocoding APIにリクエスト
  var options = { muteHttpExceptions: true };
  var geocodeJson = UrlFetchApp.fetch(geocodeApiUrl, options);
  
  //JSON文字列をパースしてオブジェクトにする
  var geocodeData = JSON.parse(geocodeJson.getContentText());
  
  //緯度・経度の取得
  var lat, lng;
  if (geocodeData.status == "OK"){
    
    lat = geocodeData.results[0].geometry.location.lat;
    lng = geocodeData.results[0].geometry.location.lng;
    
  } else if(geocodeData.status == "ZERO_RESULTS") {
    return "【Geocoding API】検索結果が0件です。";
  } else if(geocodeData.status == "ERROR") {
    return "【Geocoding API】サーバ接続に失敗しました。";
  } else if(geocodeData.status == "INVALID_REQUEST") {
    return "【Geocoding API】リクエストが無効でした。";
  } else if(geocodeData.status == "OVER_QUERY_LIMIT") {
    return "【Geocoding API】リクエストの利用制限回数を超えました。";
  } else if(geocodeData.status == "REQUEST_DENIED") {
    return "【Geocoding API】サービスが使えない状態でした。";
  } else if(geocodeData.status == "UNKNOWN_ERROR") {
    return "【Geocoding API】原因不明のエラーが発生しました。";
  }
  
  //【Places API】検索エリアのお店情報取得
  var placeApiUrl = "https://maps.googleapis.com/maps/api/place/nearbysearch/json";
  placeApiUrl += "?key=" + apiKey;
  placeApiUrl += "&location=" + lat + "," + lng;
  placeApiUrl += "&radius=" + radius;
  placeApiUrl += "&types=restaurant";
  placeApiUrl += "&keyword=" + encodeURIComponent(keyword);
  placeApiUrl += "&language=ja";
  
  //Places APIにリクエスト
  options = { muteHttpExceptions: true };
  var placeJson = UrlFetchApp.fetch(placeApiUrl, options);
  
  //JSON文字列をパースしてオブジェクトにする
  var placeData = JSON.parse(placeJson.getContentText());
  
  //お店情報取得
  var placesList = new Array();
  var nextPageToken = undefined;
  if (placeData.status == "OK"){
    
    //resultsをplacesList配列に結合
    placesList = placesList.concat(placeData.results);
    //next_page_tokenを取得
    nextPageToken = placeData.next_page_token;
    
  } else if(placeData.status == "ZERO_RESULTS") {
    return "【Places API】検索結果が0件です。";
  } else if(placeData.status == "ERROR") {
    return "【Places API】サーバ接続に失敗しました。";
  } else if(placeData.status == "INVALID_REQUEST") {
    return "【Places API】リクエストが無効でした。";
  } else if(placeData.status == "OVER_QUERY_LIMIT") {
    return "【Places API】リクエストの利用制限回数を超えました。";
  } else if(placeData.status == "REQUEST_DENIED") {
    return "【Places API】サービスが使えない状態でした。";
  } else if(placeData.status == "UNKNOWN_ERROR") {
    return "【Places API】原因不明のエラーが発生しました。";
  }
  
  //next_page_tokenが取得された場合は次ページあり。
  //next_page_tokenが取得できなくなるまで、
  //次ページ情報の取得を繰り返す。
  while (nextPageToken != undefined){
    //2秒程間隔をおく(連続リクエストすると取得に失敗する)
    Utilities.sleep(2000);
    
    //【Places API】次ページのお店情報取得
    placeApiUrl = "https://maps.googleapis.com/maps/api/place/nearbysearch/json";
    placeApiUrl += "?key=" + apiKey;
    placeApiUrl += "&pagetoken=" + nextPageToken;
    
    //Place APIにリクエスト
    options = { muteHttpExceptions: true };
    placeJson = UrlFetchApp.fetch(placeApiUrl, options);
    
    //JSON文字列をパースしてオブジェクトにする
    placeData = JSON.parse(placeJson.getContentText());
    
    if (placeData.status == "OK"){
      
      //resultsをplacesList配列に結合
      placesList = placesList.concat(placeData.results);
      //next_page_tokenを取得
      nextPageToken = placeData.next_page_token;
      
    } else {
      nextPageToken = undefined;
    }
  }
  
  //ソートを正しく行うため、
  //ratingが設定されていないものを
  //一旦「-1」に変更する。
  for (var i = 0; i < placesList.length; i++) {
    if (placesList[i].rating == undefined){
      placesList[i].rating = -1;
    }
  }
  
  //ratingの降順でソート(連想配列ソート)
  placesList.sort(function(a,b){
    if(a.rating > b.rating) return -1;
    if(a.rating < b.rating) return 1;
    return 0;
  });
  
  //placesList配列をループして
  //結果表示のHTMLタグを組み立てる
  var resultHTML = "<ol>\n";
  
  for (var i = 0; i < placesList.length; i++) {
    var name = placesList[i].name;
    var vicinity = placesList[i].vicinity;
    var rating = placesList[i].rating;
    
    //ratingが-1のものは「---」に表示変更
    if (rating == -1) rating = "---";
    
    //表示内容(評価+名称)
    var content = "【" + rating + "】 " + name;
    
    //詳細表示のリンク作成
    resultHTML += "<li>\n";
    resultHTML += "<a href=\"https://maps.google.co.jp/maps?q=" + encodeURIComponent(name + " " + vicinity) + "&z=15&iwloc=A\"";
    resultHTML += " target=\"_blank\">" + content + "</a>\n";
    resultHTML += "</li>\n";
  }
  
  resultHTML += "</ol>";
  
  return resultHTML;
}
【編集注】
Places APIの結果が英語で取得されることが多くなったため、nearbysearchにパラメータ「language=ja」を追加しました。

ポイント解説

[1〜10行目]
/*
 HTTP GETをハンドリング
*/
function doGet(e){
  //検索場所
  var address = e.parameter.address;
  //検索キーワード
  var keyword = e.parameter.keyword;
  //検索範囲(メートル)
  var radius = e.parameter.radius;
GETのHTTPリクエストを受け取ると、doGetメソッドが呼び出されます。
「e.parameter」から、GETパラメータを取得します。

[30〜37行目]
//【Geocoding API】住所→緯度・経度
var geocodeApiUrl = "https://maps.googleapis.com/maps/api/geocode/json";
geocodeApiUrl += "?key=" + apiKey;
geocodeApiUrl += "&address=" + encodeURIComponent(address);

//Geocoding APIにリクエスト
var options = { muteHttpExceptions: true };
var geocodeJson = UrlFetchApp.fetch(geocodeApiUrl, options);
「Geocoding API」を呼び出して、住所(ランドマーク名)から緯度・経度を取得します。
UrlFetchApp.fetchでURLを呼び出すときは、「muteHttpExceptions: true」を付けて、エラー発生時にもレスポンスが取得できるようにしてください。

[39〜40行目]
//JSON文字列をパースしてオブジェクトにする
var geocodeData = JSON.parse(geocodeJson.getContentText());
APIの返却結果のJSON文字列をパースして、GASで扱いやすようにJSONオブジェクトに変換します。

[42〜61行目]
//緯度・経度の取得
var lat, lng;
if (geocodeData.status == "OK"){
  
  lat = geocodeData.results[0].geometry.location.lat;
  lng = geocodeData.results[0].geometry.location.lng;
  
} else if(geocodeData.status == "ZERO_RESULTS") {
  return "【Geocoding API】検索結果が0件です。";
} else if(geocodeData.status == "ERROR") {
  return "【Geocoding API】サーバ接続に失敗しました。";
} else if(geocodeData.status == "INVALID_REQUEST") {
  return "【Geocoding API】リクエストが無効でした。";
} else if(geocodeData.status == "OVER_QUERY_LIMIT") {
  return "【Geocoding API】リクエストの利用制限回数を超えました。";
} else if(geocodeData.status == "REQUEST_DENIED") {
  return "【Geocoding API】サービスが使えない状態でした。";
} else if(geocodeData.status == "UNKNOWN_ERROR") {
  return "【Geocoding API】原因不明のエラーが発生しました。";
}
JSONデータのstatusが「OK」の場合は、結果が正常に受け取れています。
JSONオブジェクトを辿って、緯度・経度を取得しましょう。

statusが「OK」以外の時はエラーなので、エラーメッセージを返します。
UrlFetchApp.fetchで「muteHttpExceptions: true」を付けていない場合は、エラーが発生してエラーのレスポンスは取得できません。

[63〜77行目]
//【Places API】検索エリアのお店情報取得
var placeApiUrl = "https://maps.googleapis.com/maps/api/place/nearbysearch/json";
placeApiUrl += "?key=" + apiKey;
placeApiUrl += "&location=" + lat + "," + lng;
placeApiUrl += "&radius=" + radius;
placeApiUrl += "&types=restaurant";
placeApiUrl += "&keyword=" + encodeURIComponent(keyword);
placeApiUrl += "&language=ja";

//Places APIにリクエスト
options = { muteHttpExceptions: true };
var placeJson = UrlFetchApp.fetch(placeApiUrl, options);
「Places API」のnearbysearchを呼び出して、検索条件のお店情報を取得します。
keywordには全角文字が入る可能性があるので、encodeURIComponentでURLエンコードをしておきましょう。

[103〜120行目]
//next_page_tokenが取得された場合は次ページあり。
//next_page_tokenが取得できなくなるまで、
//次ページ情報の取得を繰り返す。
while (nextPageToken != undefined){
  //2秒程間隔をおく(連続リクエストすると取得に失敗する)
  Utilities.sleep(2000);
  
  //【Places API】次ページのお店情報取得
  placeApiUrl = "https://maps.googleapis.com/maps/api/place/nearbysearch/json";
  placeApiUrl += "?key=" + apiKey;
  placeApiUrl += "&pagetoken=" + nextPageToken;
  
  //Places APIにリクエスト
  options = { muteHttpExceptions: true };
  placeJson = UrlFetchApp.fetch(placeApiUrl, options);

  //JSON文字列をパースしてオブジェクトにする
  placeData = JSON.parse(placeJson.getContentText());
JSONデータのnext_page_tokenが取得できた場合は、次ページ情報があると判断できます。
nearbysearchの「pagetoken」パラメータにnext_page_tokenを渡して、次ページ情報を取得しましょう。
next_page_tokenの値が取得できなくなるまで、次ページ情報の取得を繰り返します。

次ページ情報の取得は、連続リクエストすると上手く結果が得られませんので、Utilities.sleepで2秒程待機してから行います。

[134〜148行目]
//ソートを正しく行うため、
//ratingが設定されていないものを
//一旦「-1」に変更する。
for (var i = 0; i < placesList.length; i++) {
  if (placesList[i].rating == undefined){
    placesList[i].rating = -1;
  }
}

//ratingの降順でソート(連想配列ソート)
placesList.sort(function(a,b){
  if(a.rating > b.rating) return -1;
  if(a.rating < b.rating) return 1;
  return 0;
});
ratingの降順でソートし、ランキング表示を実現します。
ソートの前に、ratingが設定されていないものをundefinedかどうかで判定して、「-1」としておきましょう。

ソートでは、sortで「連想配列ソート」を行います。

さて、ここまでの解説では、PHPのソースと同様のことを説明しています。
使うメソッドや記述が若干異なるだけで、ほぼほぼPHPソースからの焼き直しです。

「GAS」ならではの注意点は以下です。
[14〜17行目]
//HTMLを返却
var output = HtmlService.createHtmlOutput(resultHTML);
output.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
return output;
HTMLを返却する場合は、HtmlService.createHtmlOutputメソッドを通して返却します。
この時、setXFrameOptionsMode「HtmlService.XFrameOptionsMode.ALLOWALL」と指定することが重要。
この指定により、クライアントサイドのiframeに結果を表示できるようになります。

完成したGASプログラムのURLをiframeに指定して、結果表示してみます。
検索場所:
KeyWord:
検索範囲:
★結果★
「GAS」での実装も無事できました!
PHPと同様の結果が得られたと思います。

【まとめ】

サーバサイド・スクリプトで、Google Maps APIを使ったお店ランキング表示をしてみると・・・
自作したWebサービスで【LINE BOT】を作りたくなった!

次回予告。お店ランキング表示のLINEボットを作成します!!

3

あなたへのおすすめ記事