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

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

(2025年3月29日)Leafletアクセス可否チェック追加, 使用ライブラリ更新
(2024年12月15日)使用ライブラリ更新
(2024年8月31日)使用ライブラリ更新

目次

サンプル・プログラム

圧縮ファイルの内容
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.1.0 2025/03/29 Leafletアクセス可否チェック追加, 使用ライブラリ更新
1.0.2 2024/12/15 使用ライブラリ更新
1.0.1 2024/08/31 使用ライブラリ更新
1.0.0 2024/04/29 初版
pahooGeocode.cpp 更新履歴
バージョン 更新日 内容
1.9.0 2025/03/16 HTTPステータス・エラーをキャッチアップ
1.8.1 2025/02/23 setError() -- bug-fix
1.8.0 2024/05/03 getMyPath()をapikey.appのgetMyPath()関数に変更
1.7.0 2024/04/27 Edge対応に伴いデバッグ用コードを廃棄
1.6.0 2023/07/02 getPointsGSI()メソッド追加
mystrings.cpp 更新履歴
バージョン 更新日 内容
1.3.1 2025/03/16 readWebContents() リダイレクト有効に
1.3.0 2025/03/16 readWebContents() 引数httpStatus追加
1.2.0 2024/05/06 getModulePath() 追加
1.12 2021/01/31 readWebContents() 引数post追加
1.11 2020/10/17 htmlspecialchars() 追加
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" を利用してほしい。

解説:定数など

searchcafewin.cpp

  43: // 定数など ==================================================================
  44: #define MAKER       "pahoo.org"         // 作成者
  45: #define APPNAME     "searchcafewin"     // アプリケーション名
  46: #define APPNAMEJP   "喫茶店を検索"      // アプリケーション名(日本語)
  47: #define APPVERSION  "1.1.0"             // バージョン
  48: #define APPYEAR     "2024-25"           // 作成年
  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サービスの呼び出し

searchcafewin.cpp

 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:     int httpStatus = 0;
 417:     bool res = readWebContents(this->webapi, UserAgent, &contents, &httpStatus);
 418:     if (res == FALSE) {
 419:         setError(_SW("ホットペッパー グルメサーチAPI の接続エラーが発生しました"));
 420:         return (-1);
 421:     } else if ((httpStatus < 200|| (httpStatus > 299)) {
 422:         setError(_SW("ホットペッパー グルメサーチAPI の接続エラーが発生しました"));
 423:         return (-1);
 424:     }
 425: 
 426:     // 配列の初期化
 427:     for (int i = 0i < __SIZE_PPOINTSi++) {
 428:         this->Cafes[i].id = 0;
 429:         this->Cafes[i].name = this->Cafes[i].address = L"";
 430:         this->Cafes[i].latitude = this->Cafes[i].longitude = 0.0;
 431:         this->Cafes[i].url  = this->Cafes[i].photo = "";
 432:         this->Cafes[i].open = this->Cafes[i].close = L"";
 433:         this->Cafes[i].wifi = this->Cafes[i].budget = L"";
 434:     }
 435: 
 436:     // XML読み込み
 437:     int cnt = 0;
 438:     try {
 439:         std::stringstream ss;
 440:         ss.str("");
 441:         ss << contents;
 442:         ptree pt;
 443:         xml_parser::read_xml(ss, pt);
 444: 
 445:         // 応答チェック
 446:         if (optional<string>str = pt.get_optional<string>("results.results_returned")) {
 447:             int n = stoi(str.get());
 448:             if (n <0) {
 449:                 return 0;
 450:             }
 451:         } else {
 452:             setError(_SW("ホットペッパー グルメサーチAPI の応答エラーが発生しました"));
 453:             return (-1);
 454:         }
 455: 
 456:         // XML解釈
 457:         try {
 458:             for (auto it : pt.get_child("results")) {
 459:                 if (cnt >__SIZE_PPOINTS) {
 460:                     break;
 461:                 }
 462:                 // 読み飛ばし
 463:                 if (it.first !"shop") {
 464:                     continue;
 465:                 }
 466:                 // 識別子
 467:                 this->Cafes[cnt].id = cnt + 1;
 468:                 // 店名
 469:                 if (optional<string>name = it.second.get_optional<string>("name")) {
 470:                     this->Cafes[cnt].name = _UW(name.value());
 471:                 }
 472:                 // 住所
 473:                 if (optional<string>address = it.second.get_optional<string>("address")) {
 474:                     this->Cafes[cnt].address = _UW(address.value());
 475:                 }
 476:                 // 緯度
 477:                 if (optional<string>lat = it.second.get_optional<string>("lat")) {
 478:                     this->Cafes[cnt].latitude = stod(lat.value());
 479:                 }
 480:                 // 経度
 481:                 if (optional<string>lng = it.second.get_optional<string>("lng")) {
 482:                     this->Cafes[cnt].longitude = stod(lng.value());
 483:                 }
 484:                 // 営業時間
 485:                 if (optional<string>open = it.second.get_optional<string>("open")) {
 486:                     this->Cafes[cnt].open = _UW(open.value());
 487:                 }
 488:                 // 定休日
 489:                 if (optional<string>close = it.second.get_optional<string>("close")) {
 490:                     this->Cafes[cnt].close = _UW(close.value());
 491:                 }
 492:                 // PC向けURL
 493:                 if (optional<string>url = it.second.get_optional<string>("urls.pc")) {
 494:                     this->Cafes[cnt].url = url.value();
 495:                 }
 496:                 // 写真URL
 497:                 if (optional<string>photo = it.second.get_optional<string>("photo.pc.s")) {
 498:                     this->Cafes[cnt].photo = photo.value();
 499:                 }
 500:                 // ディナー予算
 501:                 if (optional<string>budget = it.second.get_optional<string>("budget.average")) {
 502:                     this->Cafes[cnt].budget = _UW(budget.value());
 503:                 }
 504:                 // Wi-Fi有無
 505:                 if (optional<string>wifi = it.second.get_optional<string>("wifi")) {
 506:                     this->Cafes[cnt].wifi = _UW(wifi.value());
 507:                 }
 508:                 cnt++;
 509:             }
 510:         // XML解釈エラー
 511:         } catch(ptree_bad_path& e) {
 512:             setError(_SW("ホットペッパー グルメサーチAPI の検索エラーが発生しました"));
 513:             return (-1);
 514:         }
 515: 
 516:     // 読み込みエラー
 517:     } catch(xml_parser_error& e) {
 518:         setError(_SW("ホットペッパー グルメサーチAPI の接続エラーが発生しました"));
 519:         return (-1);
 520:     }
 521:     contents.clear();
 522: 
 523: /** debug
 524:     cout << "cnt = " << cnt << endl;
 525:     for (int i = 0; i < cnt; i++) {
 526:         cout << _WS(this->Cafes[cnt].name) << endl;
 527:     }
 528: **/
 529: 
 530:     return cnt;
 531: }

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

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

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

参考サイト

(この項おわり)
header