C++ で喫茶店を検索する

(1/1)
>C++で喫茶店を検索する
インターネット経由で ットペッパーの「グルメサーチAPI」にアクセスし、地図や住所、ランドマーク、郵便番号等から喫茶店を検索し、一覧表示する。一覧情報をクリップボードにコピーしたり、CSV形式ファイルに保存することができる。また、ユーザーがAPIキーを取得することでGoogleマップやGoogle住所検索も利用できるようになる。
PHPでホットペッパーを利用して喫茶店を検索する」で作ったPHPプログラムをC++に移植したものである。

目次

サンプル・プログラム

圧縮ファイルの内容
searchcafewin.msiインストーラ
bin/searchcafewin.exe実行プログラム本体
bin/cwebpage.dll
bin/libcurl.dll
実行時に必要になるDLL
bin/etc/help.chmヘルプ・ファイル
sour/searchcafewin.cppソース・プログラム
sour/resource.hリソース・ヘッダ
sour/resource.rcリソース・ファイル
sour/application.icoアプリケーション・アイコン
sour/mystrings.cpp汎用文字列処理関数など(ソース)
sour/mystrings.h汎用文字列処理関数など(ヘッダ)
sour/pahooGeocode.cpp住所・緯度・経度に関わるクラス(ソース)
sour/pahooGeocode.hpp住所・緯度・経度に関わるクラス(ヘッダ)
sour/apikey.cppAPIキーの管理(ソース)
sour/apikey.hppAPIキーの管理(ヘッダ)
sour/WebView2.hWebView2に関わるヘッダ
sour/event.hWebView2用インターフェース(ヘッダ)
sour/event.cppWebView2用インターフェース(ソース)
sour/pahooWebView2.cppWebView2に関わる関数(ソース)
sour/pahooWebView2.hppWebView2に関わる関数(ヘッダ)
sour/makefileビルド
searchcafewin.cpp 更新履歴
バージョン 更新日 内容
1.0.0 2024/04/29 初版
pahooGeocode.cpp 更新履歴
バージョン 更新日 内容
1.8.0 2024/05/03 getMyPath()をapikey.appのgetMyPath()関数に変更
1.7.0 2024/04/27 Edge対応に伴いデバッグ用コードを廃棄
1.6.0 2023/07/02 getPointsGSI()メソッド追加
1.5 2022/09/03 デバッグコード埋め込み
1.4 2021/05/01 makeMapLeaflet() 引数追加
mystrings.cpp 更新履歴
バージョン 更新日 内容
1.12 2021/01/31 readWebContents() 引数post追加
1.11 2020/10/17 htmlspecialchars() 追加
1.1 2020/10/17 GetVersion2()追加,readWebContents() 引数ua追加
1.01 2020/10/03 setClipboardData() bug-fix
1.0 2020/09/22 初版
pahooWebView2.cpp 更新履歴
バージョン 更新日 内容
1.0.0 2024/04/27 初版
apikey.cpp 更新履歴
バージョン 更新日 内容
2.0.0 2024/04/29 createSetAPIkey, processSetAPIkey に統合
1.0 2020/09/30 初版

使用ライブラリ

WebAPIにアクセスするために、オープンソースのライブラリ Boost C++ライブラリcURL (カール)  および OpenSSL が必要になる。導入方法等については、「C++ 開発環境の準備」をご覧いただきたい。
また、地図表示にWebブラウザ・コントロールを利用するため "WebView2Loader.dll" を利用する。jchv / webview2-in-mingw からダウンロードできる。

リソースの準備

64ビット版の開発環境を用いる。
Eclipse を起動し、新規プロジェクト searchcafewin を用意する。
ResEdit を起動し、resource.rc を用意する。
Eclipse に戻り、ソース・プログラム "searchcafewin.cpp" を追加する。
リンカー・フラグを -Wl,--enable-stdcall-fixup -mwindows -lgdiplus -static -lstdc++ -lgcc -lwinpthread -lcurl -lssl "C:\(libcurl-x64.dllのフォルダ)\libcurl-x64.dll" "C:\(WebView2Loader.dllのフォルダ)\WebView2Loader.dll" に設定する。
また、コマンド行パターンを ${COMMAND} ${FLAGS} ${OUTPUT_FLAG} ${OUTPUT_PREFIX}${OUTPUT} ${INPUTS} -lole32 -loleaut32 -luuid にすること。

MSYS2 コマンドラインからビルドするのであれば、"makefile" を利用してほしい。

解説:定数など

  43: // 定数など ==================================================================
  44: #define MAKER       "pahoo.org"         //作成者
  45: #define APPNAME     "searchcafewin"     //アプリケーション名
  46: #define APPNAMEJP   "喫茶店を検索"      //アプリケーション名(日本語)
  47: #define APPVERSION  "1.0.0"             //バージョン
  48: #define APPYEAR     "2024"              //作成年
  49: #define REFERENCE   "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-17-01.shtm"   //参考サイト
  50: 
  51: //ListViewItemの最大文字長:変更不可
  52: #define MAX_LISTVIEWITEM    259
  53: 
  54: //ヘルプ・ファイル
  55: #define HELPFILE    ".\\etc\\help.chm"
  56: 
  57: //デフォルト保存ファイル名
  58: #define SAVEFILE    "stationsearchwin.csv"
  59: 
  60: //ホットペッパーグルメWebサービス APIキー名称
  61: #define LABEL_HOTPEPPER_API "ホットペッパーAPIキー"
  62: //ホットペッパーグルメWebサービス APIキー保存ファイル名
  63: #define FNAME_HOTPEPPER_API "HotpepperAPIkey"
  64: //ホットペッパーグルメWebサービス APIキー取得ページ
  65: #define URL_HOTPEPPER_API   "https://webservice.recruit.co.jp/register"
  66: //ホットペッパーグルメWebサービス 検索ジャンルID:カフェ・スイーツ
  67: #define HOTPEPPER_GENRE     "G014"
  68: 
  69: //マップID
  70: #define MAP_ID          "map_id"
  71: //地図の大きさ
  72: #define MAP_WIDTH       550     //地図の幅(ピクセル)
  73: #define MAP_HEIGHT      320     //地図の高さ(ピクセル)
  74: //経度・緯度(初期値)
  75: #define DEF_LONGITUDE   139.766667
  76: double Longitude = DEF_LONGITUDE;
  77: #define DEF_LATITUDE    35.681111
  78: double Latitude  = DEF_LATITUDE;
  79: //地図拡大率(初期値)
  80: #define DEF_ZOOM    13
  81: int Zoom = DEF_ZOOM;
  82: //地図の種類(初期値)
  83: #define DEF_MAPTYPE     "GSISTD"
  84: string Maptype = DEF_MAPTYPE;

とくに注意記載が無い限り、定数は自由に変更できる。

ホットペッパー グルメサーチAPI

ホットペッパー グルメサーチAPIは、入力パラメータ(IN)として GET方式を、出力結果(OUT)がXMLまたはJSONで戻るというAPIである。
WebAPIのURL
URL
https://webservice.recruit.co.jp/hotpepper/gourmet/v1/

入力パラメータ
フィールド名 要否 内  容
key 必須 APIキー
lat 必須 緯度
lng 必須 経度
range 必須 検索範囲
1: 300m
2: 500m
3: 1000m (初期値)
4: 2000m
5: 3000m
datum 任意 world: 世界測地系、tokyo: 旧日本測地系。初期値は world。
genre 任意 お店ジャンルコード。本プログラムでは定数HOTPEPPER_GENREで指定。
count 任意 検索結果の最大出力データ数を指定します。初期値:10、宰相:1、最大100。
format 任意 レスポンス形式。初期値:xml。xml または json または jsonp。
応答データ構造(json) results shop id お店ID name 掲載店名 address 住所 lat 緯度(世界測地系) lng 経度(世界測地系) open 営業時間 close 定休日 genre catch お店ジャンルキャッチ urls pc PC向けURL photo pc l PC用店舗トップ写真(大)画像URL m PC用店舗トップ写真(中)画像URL s PC用店舗トップ写真(小)画像URL wifi Wi-Fi有無 shop id お店ID name 掲載店名 address 住所 lat 緯度(世界測地系) lng 経度(世界測地系) open 営業時間 close 定休日 genre catch お店ジャンルキャッチ urls pc PC向けURL photo pc l PC用店舗トップ写真(大)画像URL m PC用店舗トップ写真(中)画像URL s PC用店舗トップ写真(小)画像URL wifi Wi-Fi有無

解説:ホットペッパーグルメWebサービスの呼び出し

 392: /**
 393:  * ホットペッパー グルメサーチAPI から必要な情報を配列に格納する
 394:  * @param   double latitude  緯度(世界測地系)
 395:  * @param   double longitude 経度(世界測地系)
 396:  * @param   int    range     検索範囲(1:300m,2:500m,3:1000m,4:2000m,5:3000m)
 397:  * @param   string genre     ジャンルID
 398:  * @return  int ヒット数/(-1):エラー発生
 399: */
 400: int pahooHotpepper::getResults_Hotpepper(double latitude, double longitude, int range, string genre) {
 401:     //ホットペッパー グルメサーチAPI呼び出し
 402:     char lat[SIZE_BUFF + 1], lng[SIZE_BUFF + 1], rng[SIZE_BUFF + 1];;
 403: 
 404:     if (range <0 || range > 5) {
 405:         setError(_SW("検索範囲が間違っています"));
 406:         return (-1);
 407:     }
 408: 
 409:     snprintf(lat, SIZE_BUFF, "%.5f", latitude);
 410:     snprintf(lng, SIZE_BUFF, "%.5f", longitude);
 411:     snprintf(rng, SIZE_BUFF, "%d", range);
 412:     this->webapi = "http://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key=" + HotpepperAPIkey + "&lat=" + (string)lat + "&lng=" + (string)lng + "&range=" + (string)rng + "&genre=" + (string)genre;
 413: 
 414:     //ホットペッパー グルメサーチAPI 応答
 415:     string contents = "";
 416:     bool res = readWebContents(this->webapi, UserAgent, &contents);
 417:     if (res == FALSE) {
 418:         setError(_SW("ホットペッパー グルメサーチAPI の接続エラーが発生しました"));
 419:         return (-1);
 420:     }
 421: 
 422:     //配列の初期化
 423:     for (int i = 0i < __SIZE_PPOINTSi++) {
 424:         this->Cafes[i].id = 0;
 425:         this->Cafes[i].name = this->Cafes[i].address = L"";
 426:         this->Cafes[i].latitude = this->Cafes[i].longitude = 0.0;
 427:         this->Cafes[i].url  = this->Cafes[i].photo = "";
 428:         this->Cafes[i].open = this->Cafes[i].close = L"";
 429:         this->Cafes[i].wifi = this->Cafes[i].budget = L"";
 430:     }
 431: 
 432:     //XML読み込み
 433:     int cnt = 0;
 434:     try {
 435:         std::stringstream ss;
 436:         ss.str("");
 437:         ss << contents;
 438:         ptree pt;
 439:         xml_parser::read_xml(ss, pt);
 440: 
 441:         //応答チェック
 442:         if (optional<string>str = pt.get_optional<string>("results.results_returned")) {
 443:             int n = stoi(str.get());
 444:             if (n <0) {
 445:                 return 0;
 446:             }
 447:         } else {
 448:             setError(_SW("ホットペッパー グルメサーチAPI の応答エラーが発生しました"));
 449:             return (-1);
 450:         }
 451: 
 452:         //XML解釈
 453:         try {
 454:             for (auto it : pt.get_child("results")) {
 455:                 if (cnt >__SIZE_PPOINTS) {
 456:                     break;
 457:                 }
 458:                 //読み飛ばし
 459:                 if (it.first !"shop") {
 460:                     continue;
 461:                 }
 462:                 //識別子
 463:                 this->Cafes[cnt].id = cnt + 1;
 464:                 //店名
 465:                 if (optional<string>name = it.second.get_optional<string>("name")) {
 466:                     this->Cafes[cnt].name = _UW(name.value());
 467:                 }
 468:                 //住所
 469:                 if (optional<string>address = it.second.get_optional<string>("address")) {
 470:                     this->Cafes[cnt].address = _UW(address.value());
 471:                 }
 472:                 //緯度
 473:                 if (optional<string>lat = it.second.get_optional<string>("lat")) {
 474:                     this->Cafes[cnt].latitude = stod(lat.value());
 475:                 }
 476:                 //経度
 477:                 if (optional<string>lng = it.second.get_optional<string>("lng")) {
 478:                     this->Cafes[cnt].longitude = stod(lng.value());
 479:                 }
 480:                 //営業時間
 481:                 if (optional<string>open = it.second.get_optional<string>("open")) {
 482:                     this->Cafes[cnt].open = _UW(open.value());
 483:                 }
 484:                 //定休日
 485:                 if (optional<string>close = it.second.get_optional<string>("close")) {
 486:                     this->Cafes[cnt].close = _UW(close.value());
 487:                 }
 488:                 //PC向けURL
 489:                 if (optional<string>url = it.second.get_optional<string>("urls.pc")) {
 490:                     this->Cafes[cnt].url = url.value();
 491:                 }
 492:                 //写真URL
 493:                 if (optional<string>photo = it.second.get_optional<string>("photo.pc.s")) {
 494:                     this->Cafes[cnt].photo = photo.value();
 495:                 }
 496:                 //ディナー予算
 497:                 if (optional<string>budget = it.second.get_optional<string>("budget.average")) {
 498:                     this->Cafes[cnt].budget = _UW(budget.value());
 499:                 }
 500:                 //Wi-Fi有無
 501:                 if (optional<string>wifi = it.second.get_optional<string>("wifi")) {
 502:                     this->Cafes[cnt].wifi = _UW(wifi.value());
 503:                 }
 504:                 cnt++;
 505:             }
 506:         //XML解釈エラー
 507:         } catch(ptree_bad_path& e) {
 508:             setError(_SW("ホットペッパー グルメサーチAPI の検索エラーが発生しました"));
 509:             return (-1);
 510:         }
 511: 
 512:     //読み込みエラー
 513:     } catch(xml_parser_error& e) {
 514:         setError(_SW("ホットペッパー グルメサーチAPI の接続エラーが発生しました"));
 515:         return (-1);
 516:     }
 517:     contents.clear();
 518: 
 519: /** debug
 520:     cout << "cnt = " << cnt << endl;
 521:     for (int i = 0; i < cnt; i++) {
 522:         cout << _WS(this->Cafes[cnt].name) << endl;
 523:     }
 524: **/
 525: 
 526:     return cnt;
 527: }

応答は XML形式である。
Boost C++ライブラリproperty_tree にある xml_parser を利用する。
今回も、cURL を使って応答情報を変数に代入し、ストリーミングを使ってxmlパーサーに流し込んでやる。

共通手順、モジュールなど

その他の関数、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。

参考サイト

(この項おわり)
header