飲食店のジャンル別にGoToEatプレミアム食事券が使えるお店を探せるサービスを作りました。(ほぼ)全国対応しています。1
軽い気持ちで作り始めたのですが、予想以上にしんどかったので知見をまとめました。似たようなサービスを作ろうと思った人の参考になれば幸いです。
特徴
-
(ほぼ)全国対応。1日1回、公式サイトのデータに追従
-
飲食店のジャンルごとにアイコン表示されるので、食べたい物から探すのが楽
-
地図描画がWebGLベースで、飲食店が多い東京都や、密集地でもサクサク動く
-
サービス名とfaviconと画面デザインがダサい2
背景
- 2020年夏〜秋にかけて栃木県佐野市に地方移住しました
- たまたま郵便局に寄ったらGoToEat食事券の発売開始日で**「25%もお得じゃん!」**と、何も考えずに5万円分購入
- 移住したてで土地勘がないため、公式サイトを見てもどこになんの店があるのかまったくわからない
- このままでは5万円分使い切るのは難しいのでは…
- リハビリがてらWebサービスでも作るか
- アドベントカレンダーのネタくらいにはなるだろ
- 仕事探しのポートフォリオ代わりにもなるし
と、思っていたのですが…(お察しください…)
構成
- 公式サイトから店舗情報をスクレイピング
- 店舗の住所をジオコーディングしてlatlngを取得
- GeoJSON形式にしてWebマップにプロット
簡単にいうとこれだけなのですが、**「お金をかけずに運用できること」**を最重視して、以下のサービス/ライブラリを選択しました。運用コストは0円です。
概要 | 利用サービス/ライブラリ | 備考 |
---|---|---|
スクレイピング | Scrapy, pyppeteer | pyppeteerはSPAの大分県だけに利用 |
ジオコーディング | PyDAMS | ジオコーダDAMSのPython binding(OSS) |
CI兼スケジューラ | GitHub Actions, GHCR | クローラ、ジオコーダをそれぞれDockernizeしてGHCRに押し込んだものをGitHub Actionsで定期実行 |
フロント開発 | Vue.js(2.x) + tailwindcss | 静的ホスティングでの運用を前提 |
静的ホスティング | GitHub Pages | 独自ドメイン利用なし |
Webマップ | Geolonia | Mapbox GL JS互換。2021/02現在、パブリックβ期間のため無料 |
リポジトリ構成は以下のとおりです。
リポジトリ名 | 概要 | 備考 |
---|---|---|
goto-eater-crawler | クローラ(47都道府県+αに対応) | 実行環境はdockernizeしてGHCRに格納 |
goto-eater-csv2geojson | クローラで取得したデータ(CSV)をGeoJSONに変換 | 実行環境はdockernizeしてGHCRに格納 |
goto-eater-data | データ格納用(CSV, GeoJSON) | GitHub Actionsで1日1回、 クローラとGeoJSON変換をスケジュール実行 |
goto-eater | フロント実装(WebApp) | GitHub Pagesでpublish。データはgoto-eater-dataのGeoJSONを参照 |
なお、ソースコードおよびデータはすべてGitHub上に公開してありますので、3
- 23万件の飲食店データ(汚い住所情報、精度の微妙な位置情報、謎の飲食店ジャンル情報つき)でなにかやってみたい方
- クソでかいGeoJSON(東京都は3.3万件くらい)を使ってなにかやってみたい方
- プログラミングスクールに高いお金を払ったので、1銭も払わずにWebサービスを作ってみたい方
などのお役にたてば幸いです。
開発
地図表示
栃木県のデータ(3000件程度)をプロットした時点で、ものによっては動作がかなり厳しいことがわかりました。
(追記: MarkerClusterを使うかどうかでもかなり違うようです。
参考: https://qiita.com/terukizm/items/07bea0cdd25e9b83fc11 )
どうしたもんかなと思っていたところ、たまたまGeoloniaさんがサクサク動くサービスをパブリックβとして無料で提供してくださっていたのを知り、使わせてもらうことにしました。
以下に検討したサービス/ライブラリについてまとめておきます。
サービス/ライブラリ | 無料枠 | 備考 |
---|---|---|
Glide | ∞ | ノーコードでPWAが作れるが、マーカーを大量にプロットするとまともに動かない。お店検索としてはお世辞にも使いやすいとは言えない反面、カスタマイズ性がほぼないので厳しかった。 |
Google マイマップ | ∞ | 奈良県、群馬県等の公式サイトでも運用されている。おそらくGUI以外の編集手段がなく、データ更新時の追従が大変。 |
Folium | ∞ | OSS。簡単にhtmlを吐き出せるので少量のプロットならめちゃくちゃ楽だが、Leafletベースのためかマーカー数が1000件を超えたあたりから画面スクロール等の挙動が厳しくなった(MarkerClusterを使えば動作は問題なかったが、HTMLのファイルサイズがかなり凶悪だった) |
GitHub | ∞ | GeoJSON形式でcommitしたものをブラウザ経由でプレビューするとOpenStreetMapベースの地図にプロットされる。簡単だが多少のデザインカスタマイズ以上のことはできず、巨大なGeoJSONだとレンダリングされない。topoJSONだとファイルサイズが削減できるこのことで試してみたが、今回のように全部Pointベースの場合、恩恵はなかった。 |
Geolonia | ∞(パブリックβ) | OpenStreatMap(OpenMapTiles)を基盤としたベクトルタイル+WebGLレンダリングのWebマップを提供するホスティングサービス。ほぼzero-configのため導入コストが低い一方で、 (Mapbox GL JSが扱えれば)自由度の高いカスタマイズも可能。地図デザインもなにもせずともいい感じで見やすく、表示もサクサクと、パブリックβで無料というのが申し訳ないサービス。 |
MapTier.jp | 100,000件/月 | Mapbox互換ということで目をつけていたが、Geoloniaで満足してしまったので未調査。関係ないがMapbox GL JSを触るにあたってMIERUNE社の中の人のBlogはめちゃくちゃお世話になると思う。なった。 |
Mapbox | 50,000/月 | 最近はもうGoogle様より厳しい…? |
Google Map | 100,000/月 | 無料枠$200 の2$ /1000件として計算。お金があれば。 |
2021/02/12 補足:
Geoloniaの中の人から補足で、API_KEYにYOUR-API-KEY
という文字列を指定した場合、GitHub Pages、localhost:*、codepen.ioからのアクセスに接続元が絞られますが、ユーザ登録とかも不要なので、オープンソースプロジェクトや実験環境で積極的かつ気軽に使ってくださいとのことでした。菩薩かなにか?
クローラ
公式サイトから飲食店情報を取得するクローラ実装には、使い慣れていたScrapyをメインで利用しました。requests + bs4派閥も根強いですが、HTTPキャッシュ周りがbuilt-inされていて作り込みをしないで済むという1点だけでも、Scrapyを使うメリットがあります。
ただし、大分県のGoToEat公式サイトだけはSPA(Angular製)だったため、scrapy-splushを使うのではなく、個別にpyppeteerを使って実装しています。
pyppeteerについては正直枯れてないというか、ちょっとハマりやすい部分もいくつかあったので、無難にseleniumとかを使うほうが良かったのかなという気もしています。(1サイトだけだからなんとかなりましたが、もう少し量があったら諦めていたかもしれません…)
また、今回は各都道府県の公式サイトの仕様により「jsonから読み込む」「csvから読み込む」「pdfファイルから読み込む」「xlsxファイルから読み込む」といった多彩な処理が要求されましたが、そのあたりは潤沢なPythonライブラリが使えるので困りませんでした。
(**ただし、PDFをデータソースとして読み込むのは最終手段としてください。**読み込むPDFのレイアウトに依存し、ライブラリ選定と試行錯誤がほぼ必須となるので、とにかくしんどいです)
-
Scrapy Note・・・製作中 - Scrapy Note
- 初心者にもわかりやすいくらい丁寧で、実践的なサンプルが例示されてるのでおすすめです。
- リリース応援しております
-
pyppeteerの使い方 – rinoguchiブログ
- Linux上で実行するときのノウハウとかめちゃくちゃ助かりました
-
Go To Eat 北海道キャンペーンの取扱店リストのPDFをCSVに変換 - Qiita
- 現在ではWebサイトから店舗一覧が提供されているためこの仕様ではなくなっていますが、PDFからの処理には時にこういった苦労が必要だったということで…
- 筆者さんの他のGoTo関係の記事は「あー、私もこれやったわ…」っていう気持ちになりました
ジオコーダ
クローラによって全国23万件程度(2021/02/12現在、徳島県を除く)の店舗データが取得できましたが、無料でこの量のデータを処理できるジオコーダは限られてしまいます。
以下に検討したジオコーダの一覧を示します。
ジオコーダ | 無料枠で処理できるデータ量 | 備考 |
---|---|---|
Google Geocoding API | 40,000/月 |
$ 5/1000件 として計算。 月額$ 200 まで無料。ただし利用規約上、ジオコーディング結果のlatlngを保持できるのは30日まで、地図にマッピングするにはGoogle Mapsを使わないといけない。(それはまた別課金) |
Yahoo!ジオコーダAPI | 50,000/日 | ただしAPI経由で取得したデータを二次利用できない |
Mapbox Temporary Geocoding API | 100,000/月 | 枠は広いがGoogle Maps同様にMapboxの地図上に表示させる必要があり、ジオコーディング結果を永久保存することはできない...?(利用規約がちょいちょい変更されてるせいかいまいちよくわからない) |
NAVITIME Geocoding | 500 / 月 | 無料プランはRakuten Rapid API経由のみ、枠が狭い |
Geocoding.jp API | 10秒/1回(≒23日間) | 個人サービスなので中の人のtwitterをみる限り20万件叩くのは人道に反する(がんばってください…) |
Azure Maps | STANDARD S0で25,000/月? | 日本では市区町村レベルの位置情報しか対応していないのでダメ |
Amazon Location Service (Preview) | 10,000 / 月 | 2020/12/17発表のため当初は未検討。内部的にEsri(ArcGIS?)とHEREが使え、Esriは挙動が大変素直で良かったが、利用規約的にジオコーディング結果をGeoJSONにして一般公開するのは難しそう。 |
ぐるなびWebサービス レストラン検索API | ∞(7,200/時間) | 存在を知らず当初は未検討。GeoJSON化しての保存が利用規約 第9条に抵触する可能性があること、ぐるなびに登録されていない店舗情報の場合は値が取れない(東京都で見た限りファストフード店を始めとして結構あった)ことから見送り。2021年6月30日に無料APIサービスの提供を終了するタイミングがGoToEatの終了タイミングと一緒でニコニコしている。 |
無料枠に収めることはもちろん、利用規約で位置情報の二次利用4が難しそうなものが多いです。そのため、制限が少ないOSS製のジオコーダについても調査しました。
ジオコーダ | ライセンス | 備考 |
---|---|---|
ジオコーダDAMS | FreeBSD | 同梱データは国土交通省の位置参照情報(2012年度版)を元にしており古く、街区レベル(「○○町△丁目□番」)の解像度までしか取得できない。東京大学空間情報科学研究センターのシンプルジオコーディングサービスや、地理院地図でもこれが使われてるっぽい。pydamsというPython Bindingがある |
GeoNLP | FreeBSD | 内部的にDAMSを利用しているっぽい。現在大規模リニューアル中とのこともあり未検証 |
IMIコンポーネントツール | MIT | 国土交通省の位置参照情報を利用しており、大字・町丁目レベル(「○○町△丁目」)の解像度までしか取得できない |
Community Geocoder | MIT | MITライセンスのOSS。内部的にIMI コンポーネントツールを利用してるっぽい |
結論として、料金の面と利用規約の面から、今回の要件で利用可能なのはDAMSのみでした。5 ジオコーディング精度については微妙ですが、これはもう仕方ないなという感じです。
例: "東京都墨田区江東橋3丁目14番地5号 錦糸町駅ビル 5F"
-> "東京都/墨田区/江東橋/三丁目/14番"
住所に対する前処理(正規化)
DAMSで取得した住所に対してジオコーディングしたところ、正しくジオコーディングできない場合がありました。
概要 | 例 | 対策 |
---|---|---|
ビル名・マンション名に影響されてしまう | "加賀ビルディング"など | 住所から番地以降、ビル名やマンション名以下を削除 |
都道府県名が省略されているが、同名の市区町村が存在する | "東京都調布市/広島県調布市"など | クローラが都道府県単位なので、処理結果から都道府県名を補完 |
政令指定都市において、区名が省略されている | "大阪市東心斎橋" -> "大阪市東区" | "大阪市中央区東心斎橋" なら正しく取れる (未対処) |
政令指定都市において、市名が省略されている | "静岡県清水区" -> "静岡県清水市(旧地名)" | "静岡県静岡市清水区"なら正しく取れる (未対処) |
無番地 | スキー場(国有林中)に多い | (対処不可) |
駅、空港、キャンプ場、ホテル、ショッピングモールなどの施設内 | 大口事業所個別番号が払い出されてそうな住所で、住所を省略していきなりホテル名とかが出てくる | (対処不可) |
そもそも入力されてる住所が間違ってる | 「三重県のサイトに、愛知県名古屋市のデータが入っている」など | (対処不可) |
試行錯誤の結果、ビル名以下の削除と都道府県名の補完の前処理を追加しました。
以下のようなものはDAMS側である程度、よしなに処理してくれるようです。
-
3丁目4番地5号
形式と3-4-5
形式(漢数字/全角半角を含む) - 異体字
- 郡/大字/小字の有無
住所処理の手法、複雑怪奇な日本の住所の概念理解には、以下が参考になりました。
- 日本の住所の正規化に本気で取り組んでみたら大変すぎて鼻血が出た。 - Qiita
- 日本の住所処理で鼻血を出さないために - Qiita
- メモ:住所から番地以下を除去する - Qiita
- 住所を「都道府県」「市区町村」「それ以降」に分ける - Qiita
- 番地の謎 今尾恵介 | 知恵の森文庫 | 光文社
この辺もとにかく厳しいのですが、入力データの汚さと、DAMSの解像度の限界もあるので、ある程度割り切っていくしかないという結論に至りました。
ジャンル
飲食店のジャンル分類(例: 居酒屋、和食、カフェ)についても、各都道府県で独自仕様になっています。
都道府県例 | 備考 | |
---|---|---|
ジャンル概念なし | 青森県、神奈川県、鹿児島県など | liny系、premium-gift系。全部「その他」扱いとした |
ジャンル指定あり | (だいたいこれ) | 分類粒度は10〜30で幅がある。地域色みたいなのが出てたりしてたのしい |
複数ジャンル指定あり | 福井県、香川県、徳島県など | 本サービスでは先頭の1件のみ利用 |
自由入力 | 岩手県、東京都 | カオス |
都道府県ごとに分類がバッティングしているものもあり、またフロント側実装を簡単にするためにも、これらを統一する必要がありました。
結論としてはアドホックにゴリゴリと分類ロジックを書き、力技で埼玉県公式サイトの分類基準をベースとした全10ジャンル6へと再分類しています。
GeoJSON生成
取得した店舗情報(店舗名、住所、etc...)とジオコーディングで取得した位置情報(latlng)を元に、ジャンル別にGeoJSONを作成しています。
これはPythonの場合便利なライブラリがあるので、それを使うと楽です。
本番環境用にはminify、開発環境用にはpretty+デバッグ情報を付与したものを出し分けてあげると便利でした。
あとは住所エラー、各種書式エラー(公式URL、電話番号等)のバリデーションロジックを入れ、各都道府県単位でエラーになっているデータをリストアップしたりしています…が、ほとんど元データの問題なので正直どうしようもないのと、あまりに量が多いので、作り込んだはいいけどほとんど確認できていなかったりします。
かなしい。
GitHub Actionsによる定期実行
作成したクローラーと、GeoJSON作成スクリプトはDockernizeした上で
GitHub Container Registry(ghcr.io)に突っ込み、GitHub Actionsのschedule機能を使って深夜時間帯に定期実行させています。
一連の処理が正常終了したら、成果物をリポジトリに格納。PRを作成してレビュー依頼が飛ぶようにしており、毎朝内容とか差分を目視で軽く確認してから、手動でmergeしています。 7
参考:
- 新 GitHub Actions 入門 - 生産性向上ブログ
- Github ActionsでGithub Container RegistryにDocker imageをpushする最小Workflow - Qiita
- Github Actionで定期的にファイルをリポジトリにコミットさせる – nozograph
- [GitHub]Actionsの動作確認時は忘れずにACTIONS_RUNNER_DEBUGとACTIONS_STEP_DEBUGを設定しよう | Developers.IO
フロント実装
定期実行でデータが日々取得できれば、あとはリポジトリに格納したGeoJSONのURLをそのままデータソースとして読み込んで、いい感じに表示するだけです。
私はフロントエンドが苦手かつMapbox GL JSをまったく触ったことがなかったので、最初はHTML+CSS+Pure JSで地図表示やマーカー表示、ジャンル別の出し分けといった基本機能を動作確認するためのPoCを作成し、その後Vue.js(2.x) + TailwindCSSで作り直しました。
遠回りではありますが、最初からVue.jsとかReactで作ってたら間違いなく挫折してたと思うので、フロント苦手勢にはおすすめです。
- PoC (モバイル対応考えてなくてひどいことになっていた)
あとは最初から以下の点に気を使っておけば、もう少し楽に開発できたのかなと。フロント側を普段からやってる人には当たり前のことばかりかと思いますが…
-
モバイルファーストでデザイン/設計/実装する
- 制限が厳しいとこからやるべき、特に地図アプリは画面のほとんどが地図で埋まる
- Mobile Safariだと100vh指定したときにアドレスバーの高さのせいで地図の下端(しかもライセンス表示が入ってるとこ)が切れて辛かった
- ちゃんと実機で確認すること
- デザインは類似サービスからパクる
- デザイン苦手な人が頭使っても無駄
- その道のスペシャリストがいろんなことを考えて設計しているものなので、素直に真似するべき
-
最初にデザインをやって、モックを作ってから、フロント側を実装すること
- 昔ながらのサーバサイドエンジニアは機能を作ってからデザインを当てがちだが、二度手間になりがち
- Adobe XDみたいなツールも使えるなら使うと良さそう
-
JavaScript Primerをしっかり頭に叩き込む
- 文法とか構文レベルの問題でハマらないようにしておく
- Promise使うのなんてXHRとかsetTimeoutくらいだろと思ってたら普通にそんなことなかったので、ちゃんと一通り勉強しておく
Mapbox GL JS
Geoloniaの提供する外部jsはMapbox GL JSとほぼ互換性があるので、実装サンプルを参考にしながら頑張れば、だいたいのことはできそうです。特に以下の2つのチュートリアルが大変参考になりました。
最近はMapbox GL JSの情報量が増えてきているのを感じますので、かなりやりやすくなってきてるんじゃないかと思います。(簡単だとは言っていない)
TailwindCSS
デザインが苦手な人には悪くない選択肢で、今回なんとかかろうじてギリギリそれっぽい見た目にできた(???)のはTailwindCSSのおかげだと思っています。
意識の低いサーバサイドエンジニアが触るには、Bootstrap4とかBulmaよりも学習コストが低いんじゃないかと。tailwindcomponentsからやりたい機能と似たような部品を探して、参考にしながら実装していくのが楽かと。
あとはインテリセンス入れとくといいです。
今後の展望
ちょっとやれるか自信ないんですが… GoToEatも6月末までになってしまったので…
Google Maps Platformベースに移行
DAMS & Geolonia構成で9割がた作ってから友人に教えてもらったのですが、Google Maps Platformが新型コロナウイルス感染症(COVID-19) 関連の非営利プロジェクトに対し、クレジットの無償提供をしてくれているそうです。
ジオコーディングの精度問題はGoogle Geocoding APIを使うだけでかなり改善されるので、対応していければなと思っています。(利用規約上地図表示がGoogle Mapsに限定されてしまうため、大変お世話になったGeoloniaから乗り換えざるをえないのは非常に心苦しいのですが…)
地図表示だけでなくGoogle Place API経由で(Googleに登録されている)店舗情報やユーザレビュー、投稿された写真といった、お店を選ぶ上で有益な情報も取得できるため、ユーザ目線では、かなり使い勝手のよいものになることを期待しています。
「コロナでやばくなってる飲食店を支援する」として申請は通っているらしいので、ご期待いただければ幸いです。
公式サイトからのお知らせ内容を通知できるように
公式サイトからGoToEatに関する重要なお知らせ(緊急事態宣言が伸びたり、食事券の有効期限が伸びたり)があるのですが、正直わざわざ公式サイトを見に行く人はあんまりいないんじゃないかと。
一応現時点でも右上のメニューからリンク貼って簡単に飛べるようにはしていますが、まず毎回は見ないので、未読の公式サイトのお知らせがあったら通知してくれるような機能があったら便利かなと思っています。
DAMSの辞書データをアップデート
流石に2012年度データは古すぎるので、最新の位置参照情報にアップデートとしたいです。2020年には令和島とかいう住所も増えたらしいですし…
問題はいまいち辞書データの作り方がわからなくて、DAMSのソースコード読みながら頑張るしかなさそうなところでしょうか。
GeoJSON等の軽量化/フロント高速化
結構データ量が多いので(特に都内)、通信回線が弱いと厳しいかもしれません。フロント周りも実装がしょぼいせいで初回描画に時間かかったりしてるんじゃないかと思うので、うまくやっていけたらいいなあという感じです。
マッピング位置の個別修正対応
23万件くらいあるので個別で位置を修正するのは基本的にやりたくないんですが、原因はどうあれ、実際に存在する住所に対しての誤マッピングが多めなのは間違いないので、やるかやらないかは別として、個別に対応できるような仕組みは提供しておきたいなあ、という気持ちは、心の片隅にあったりしなくもないです。
(ただし、誠実にその辺を対応されている京都府とか福島県の公式サイトを見る限り、絶対に地獄だなという確信はあります)
さいごに
緊急事態宣言もなかなか収束する気配も見せず大変なご時世ではありますが、なにかのお役にたてば幸いです。
なお、目的の進捗ですが、
- 5万円分の食事券
-
3.5万円分ほど残ってる
- 開発がしんどすぎて外に出るのが嫌になり自炊したため
-
3.5万円分ほど残ってる
- 2020年アドベントカレンダーへの投稿
- 2月になったので2021年を狙います
- 仕事探し
- 今んとこ特に役に立っていません
という感じです。よろしくお願い申し上げます。
【→ダークサイド編】
-
徳島県は公式サイトに「※本サイトのコンテンツの無断転載を禁じます。」との一文があるので見送っています。また、静岡県と鹿児島県の一部のプレミアム商品券については対応していません。 ↩
-
当初は「GOD(or SOUL) EATER」をもじって「GOTO EATER」だったのですがダサかったので、どうせならもっとダサくしようと思ってこうなりました。 ↩
-
取得しているデータは公開情報であり問題ない(と思われる)ものに絞り、ジオコーディングについても(基本的には)ライセンス問題のないDAMSの処理結果を用いています。ただ取得データのライセンスを決めたり、勝手にオープンデータとかにしてよいのか?という部分が正直まったくわからなかったので、「公開するけど別に私のもんじゃないんで勝手に使う分には知らんぷりしますが使ってもらえるとめっちゃうれしい」みたいなスタンスにさせてください… ↩
-
運用コスト0円のためにジオコーディングしたものをGeoJSON形式にして静的ホスティングする前提でしたが、規約的にそれがどの程度許可されるのかの判断は非常に難しかったです ↩
-
IMIコンポーネント系列の解像度だと丁目単位までの精度、つまり「東京都千代田区一丁目」以下の住所が全部同じになってしまうため、お店の位置をプロットするには厳しかったです。ただし住所からだいたいの位置を取得し、地図表示の中心点として設定するには十分な精度のため、フロント実装側ではCommunity Geocoderを利用させて頂いています。 ↩
-
数が多いとアイコンと色の出し分けが大変、左袖メニューに入り切らないという理由で全10ジャンルとしました。日高屋はジャンルとしては何屋なんだ…とか、やきとり店の分類が「焼肉・焼鳥」と「居酒屋・焼鳥」の2パターンある、「ハンバーガー」が「バー(飲み屋)」と誤判定されてしまうとか、色々ありました。 ↩
-
最初はautomergeさせてたんですが、失敗してたときにrevertする方がめんどくさかったので毎日あたたかみのある作業をしています… ↩