はじめに
本編は他の人の役に立つような、わりとキレイめな話だけに絞ったので、泥臭かったり汚かったりする話はこちらに。
こっちは私が書きたくて書いてるだけなので、読んでてつまんなかったり、気分が悪くなったりしたらブラウザバックしてください。よろしくお願い申し上げます。
GoToEat公式サイトが戦時急造
GoToEatキャンペーン自体が非常に「スピード感のあるスケジュール」で発案・実施されており、自治体によっては2020/08/25の事業者決定からおよそ1ヶ月程度でのキャンペーン開始となっていました。つまり、システム開発における準備期間は1〜2ヶ月程度しかなかったと考えられます。1
この結果、12月中旬に至るまでは毎日のように47都道府県のうちどこかのサイトが更新され、そのたびにクローラの修正が必要になりました。
最初期には店舗一覧がPDFでしか提供されていないケースも多々あり、苦労してPDFから取得したのにいつの間にか検索用Webサイトがリリースされていたりして何度も悲しい気持ちに。
特に3回くらいクローラを作り直した北海道、鹿児島県、奈良県については複雑な感情があります。(鹿はともかくその刺身はどこの海から取ってきたの?)
2021年1月以降は修正も週1くらいの頻度に落ち着いてきているのでひとまず一安心ではありますが、ほとんどの都道府県ではキャンペーン期間が6月末まで伸びているので、まだ油断できません… 2
各都道府県ごとにほぼ独自の仕様/実装
同じシステムを流用している一部自治体3を除き、各都道府県でほぼ独自にWebサイトが開発されています。これはGoToEatの食事券発行委託事業が、都道府県単位での入札となっているためではないかと推測します。
このためパッと見で見た目が似てても、内部実装はほとんど別物だったり、細かい部分の仕様(ジャンル分けとか検索仕様とか)が異なっていたりしました。
結果として予想以上にクローラの使いまわしができず、40/47都道府県くらいは個別に実装しています。
検索ページを複数タブで開くとセッションが混ざる北海道
2020年に新規開設されたサイトでそんなことある…?
Scrapyでこの手のCookieベースのセッション管理(+csrf対策)に対応しようとすると意外とめんどくさく(ScrapyのリクエストはQueueに積まれて並列で走るので、エリア別でGET/POST検索クエリを発行しないとけない謎仕様とも相まって個別にCookieをstore/loadしないといけない)、2日くらい試行錯誤したあとに諦めてrequests + lxmlでクローラを実装しました。
Excelから吐き出したHTMLで運用してる鹿児島県
つよい。ちなみにこうなる前まではPDFで、それはそれで地獄でした。罫線がないレイアウトだとPDFからテーブルとして認識ができないのでその辺を力技で…
某グルメサイト上で運用してる上に件数が多い東京都
東京都の公式サイトでは2020/11/20にようやく店舗一覧が公開されたのですが、それが某グルメサイトの上にデータを置いただけのものでした。
しかも件数が33,000件以上と多く(大阪府の約2倍)、さらに詳細ページまで取得しないと住所データが取れない仕様。これはクローラのお作法である1リクエスト/1秒を厳守しても8時間以上かかってしまう計算でした。
叩きたくないなあと思いつつクローラを実装して動かしてみたのですが、取得したデータのどこまでが某グルメサイトの著作物にあたるのかがわからないため、対応はペンディング。4
最終的には12月中旬にPDF形式での店舗一覧が提供されたので、これを元にtabula-pyを使って店舗情報を抽出しています。
余談ですがこの区分、JIS X0402という規格の定義順なんですね。都民のTier表として喧嘩を売ってるのかと思った…
形が微妙な千葉県
千葉県のサイトは以下のような仕様でAPIレスポンスを元に表示していますが、野田のあたり(左上)と銚子のあたり(右端)が存在するせいで余計な場所まで見ないといけないため、
無駄な時間と負荷をかけてしまっていました。
これは都道府県メッシュを使い、メッシュ単位で処理することで解決しました。神奈川県、滋賀県についても同様に対応しています。
都道府県別500mメッシュ - 500mメッシュ_12_千葉県
一つの県に複数のGoToEatキャンペーン
**静岡県と鹿児島県については複数のGoToEat食事券が存在しており、対応しているお店が異なります。
静岡県ではおおむねどちらの商品券も利用できるのですが、片方のキャンペーンにしか対応していないお店も、無視できない程度にはありました。
データを寄せることも検討しましたが、「住所や店舗名に表記ゆれがある」「電話番号でも誤入力や片方だけ入力されている場合がある」**ため、同一店舗と判定することが非常に難しく、諦めざるを得ませんでした。
結果として最初に実装していた「ふじのくに静岡県GoToEatキャンペーン食事券」(通称・赤券)のみが本サービスの表示対象となっています。
鹿児島県の商工会議所ベースの方については件数が少なく(100件以下)、そもそも店舗住所が載ってないので対応不可でした。
料理ジャンルの選択肢の先頭にうどんを持ってくるうどん県
よくわからない勢いがある佐賀県
Scrapyのログ出力周りが使いづらい
scrapy
コマンドで叩いたときと、スクリプトからCrawlerProcess
経由でぶん回したときとで挙動を切り替えるのに試行錯誤しました。
全部標準出力に出す方式だとGitHub Actionsのログが汚れまくるのでファイルに吐いて… 途中でコケたときに追うことも考えるとリポジトリにコミットして…とかやりだしたため、非常にめんどくさかった記憶があります。
今振り返ればS3とかに吐いてしまうのが一番ラクだったのかなと。実際ほとんどログを見返すことってないですしね。(どうせ手元でRe-Runして再現するか試さないといけないのであんま意味なかった)
Scrapyのリトライが微妙
リトライ回数(RETRY_TIMES)しか指定できないっぽい。しかたないのでMiddlewareで対応しました。
https://zenn.dev/terukizm/articles/d53a536809b101
1日5個クローラ書いても47都道府県で10人日かかる
算数ができていませんでした。ちなみに1日5個はかなりうまく行ったケースです。
公式サイトの入力データがとにかく汚い
- 住所がGoogleとかに登録されているのと違っている
- 店名もGoogleとかに登録されてるのとは違っている
- 郵便番号も違っている
- 電話番号も違っている
という感じで、何が正しいんだ…というのがよくありました。クローラの結果を差分で見ててもこんな感じで、毎日結構な頻度で修正が入ってます。(最近落ち着いてきたけど1日100件くらいは普通)
公式サイトの入力データが重複
「店舗名」「住所」が同一の場合を重複として5、
- クローラの実装不具合
- 普通に公式サイトに重複入力されてる
- ソートキーがユニークでないのでページングしたときに結果が一意にならず重複してしまう
と3パターンあるため、確認作業が地味にしんどかったです。最後のはソートキーとしてOrder By にupdated_atだけを指定してたりすると初期データ投入した時にでよく起きるやつで、公式サイトに商工会議所経由で報告して直してもらったりしました。
めちゃくちゃ丁寧に返答いただいたりしてこちらこそありがとうございますという感じでした。
住所にハイフンっぽいハイフンじゃない文字が混じってる
OCRとか変なIME(Androidのだと色々あるらしい)経由で入力されたのか、パッと見ではわかんないハイフンっぽいハイフンじゃない文字が色々入力されていたりして死にました。
最終的には"|-|‐|-|‑|ー|−|‒|–|—|―|ー|"
みたいなことに。
位置がズレてても土地勘ないとわからない
知らない場所だとプロットされたものを見てても位置ズレがまったくわかりません。
**「地方移住して土地勘がないのでWebサービスを作った」**はずなのに、結果として家の近くの飲食店の場所に、異常に詳しくなりました。
本末転倒ですね。
変な住所がある
GoToEatの対象が飲食店なので、変わった住所も結構ありました。
DAMSのジオコーディング精度が出ない
DAMSで内部的に使われてる位置参照情報データの仕様上どうしようもなく、DAMS自体も「もっと細かいのが必要なら商用データとかを加工して自分で辞書データ作ってね(その割にはドキュメントとかはほとんどないけど)」っていうスタンスです。
ただ特定都道府県の公式サイトはGoogleMapへプロットするためにlatlng情報を保持しているため、これをこっそり取得して使うことで、精度を出したりしています。
どことは言いませんが、<script>
タグ中にjs配列(jsonじゃなくて)をベタにdumpしているので、それをdemjsonでparseして読み込む…といった邪悪な処理があったりします。
ジャンル名が自由入力
岩手県と東京都は申請者の自由入力をそのまま使ってるのか本当にカオス。
**「イランリョウリペルシャリョウリジャーメジャムアサガヤテン」**というジャンル名に至っては「異国での申請、大変だったんだろうなあ…」という感想しか出ませんでした。
東京都はあまりにフリーダムすぎて途中で諦めました… そもそもなんの店なのか普通にわからない店とかある…
GitHub ActionsでGitベースのワークフロー(成果物をリポジトリに放り込むタイプ)を組むと、エラー時のハンドリングがしんどい
クローラはいろんな理由で急にコケます。(サイト更新でDOMが崩れる、想定外のデータが投入された、深夜のサーバメンテナンスに出くわす、突然50xが帰ってくるなど)
そういったエラー時の対応、「クローラでは正常終了した成果物はコミットしておきたいが、異常終了した成果物については破棄したい。でもログについては確認できるようにして、再実行する場合は移住終了したクローラだけ実行できるようにしたい」というふうに、細かい運用を考え出すと仕組みが複雑になりやすいです。
他にも、基本的には対話ベースであるGitコマンドを、ファイルが存在しない場合でも問題なく処理できるように標準エラーを握りつぶしたり、変な差分が出ないように成果物をソートしておいたり、GitHubでプレビューできないような巨大なdiffが発生しないようにコミット単位を調整したり…といった、わりとセンシティブな作り込みが必要になりました。
成果物をリポジトリにコミットしてPRで処理結果を確認する仕組みは、きちんと正常系で動いてれば非常に楽で、触ってて気持ちが良く、変更履歴も残るのでいい感じではあるのですが、構築に手間がかかり、エラー時の対応がブランチワークを駆使した複雑な(属人性の高い)ものになりやすいので、一概にこれがベストとは言えないなあと思いました。
特に私の場合は、GoToEat公式サイトが安定しきっていない時期と作り込みのタイミングが重なったため、**「GitHub Actionsでワークフローを組み立てつつ、クローラがコケてれば公式サイトを確認してクローラを修正し、ジオコーディング結果を見ながら住所に対する正規化処理を調整」**というのを2週間くらい延々とやる羽目になり、とてもつらい気持ちになりました。
今思えばクローラについては0円運用にこだわらず、適当な省電力PCを常時起動させておいて、適当に定期実行させる方が圧倒的にコスパが良かったのかなと思います。失敗した場合のRe-Runとかもキャッシュから取れますしね。
GitHub Actionsのスケジュール実行が1時間くらいブレる
深夜帯はスタックしがちなのか、cron指定した時間から1時間くらい遅れて起動したりします。あんまり遅すぎると朝までに終わんなかったりするので、開始時間の調整がめんどくさかったです。
あとUTC指定の逆算。
alpine.jsからVue.jsへの移行が意外とめんどう
参考にしたTailwindCSSのコンポーネント実装サンプルに従って、TailwindCSS + alpine.jsでデザインモックに動きのある実装をしてからVue.jsに移行していました。
Vue.jsと文法は似てるんですが、そこまで無加工で移行できるってわけでもなかったので(特にx-dataとかの書き換えがだるい)、モックではデザイン面でTailwindCSSを使うだけにして、素直にVue.jsとかSPAの方で実装したほうが楽かなと思いました。
めちゃめちゃ複雑な動きを実装したい、とかならまた別なのかもしれませんが…
食い物のアイコンを探すのが難しい
カフェとかレストランくらいならそこら中にあるんですが、そこから細かい単位(焼肉とか中華とかカレーとか)になるとなかなかなくて困りました。
最終的には以下のサイトからお借りしております。
Mapbox GL JS
むっかしかったです。処理の依存関係がよくわかんなくて、読み込み終わんないタイミングで触ってコケるとかよくありました。終盤はなんとかなってきたので最終的には慣れだと思いますが。
pyppeteerがデフォルトで使ってるChromiumのバージョンが古い(71.0.3542.0)
大分県のサイトが更新され、依存ライブラリかなんかがglobalThis()を使うようになったことにより突然死するようになりました。リリース直前に。
PYPPETEER_CHROMIUM_REVISIONを指定してそれなりに新しいバージョンのChroniumで動かすようにして解決しましたが、全体的にpyppeteer、正直まだ人柱感あるなって思いました…
(2021/02/12追記)リリース予定日直前にサイト更新連打
東京、大分、奈良、大阪と今まで苦しめられた自治体の波状攻撃が、よりによってリリース予定日の直前に来ました。ロックマンのラストステージかな?
リリース日を私の誕生日にあわせたかいがありました。本当にありがとうございます。
…んなわけねーだろ!!!!!!!!!!!!!!!!! 6
おわりに
ここまでどうでもいい内容をお読みいただきまことにありがとうございました。楽しんでいただけたのでしたら幸いです。
あとGoogle Geocoding API周りとかでもいろいろ酷いネタがあったりするので、そっちも気が向いたらまとめてみたいなと思っています。
-
もちろん開発側は採択結果が出る前から準備していたと思いますが… ↩
-
リリース3日前にこの記事を書いててました。伏線です ↩
-
LINY系(神奈川県、千葉県、滋賀県)とスパイラル系(茨城県、岐阜県、三重県)はほぼ同一の仕様だったので、かろうじて使いまわしができています。 ↩
-
店舗名や住所などは公知のものなので著作物にはあたらない認識ですが、ジャンル名あたりになると微妙かなと思っていました ↩
-
住所だけだとショッピングモールなどの場合に住所がかぶる、店名だけだとよくある店名の場合にかぶる(例: 愛知県には「すずや」が複数ある)など ↩
-
まあ冷静にカレンダーの並びみたら、そりゃここの深夜でリリースぶっこみますよね。わかる。 ↩