5万円分のGoToEatプレミアム食事券を使い切るためにWebサービスを作った話 (ダークサイド編)

# はじめに

[本編](https://qiita.com/terukizm/items/60c901d0bcb0bcc70a8a)は他の人の役に立つような、わりとキレイめな話だけに絞ったので、泥臭かったり汚かったりする話はこちらに。

こっちは私が書きたくて書いてるだけなので、読んでてつまんなかったり、気分が悪くなったりしたらブラウザバックしてください。よろしくお願い申し上げます。


# GoToEat公式サイトが戦時急造

GoToEatキャンペーン自体が非常に「スピード感のあるスケジュール」で発案・実施されており、自治体によっては2020/08/25の**事業者決定からおよそ1ヶ月程度でのキャンペーン開始**となっていました。つまり、**システム開発における準備期間は1〜2ヶ月程度しかなかった**と考えられます。[^1]

[^1]: もちろん開発側は採択結果が出る前から準備していたと思いますが…

この結果、12月中旬に至るまでは**毎日のように47都道府県のうちどこかのサイトが更新され**、そのたびにクローラの修正が必要になりました。

**最初期には店舗一覧がPDFでしか提供されていない**ケースも多々あり、苦労してPDFから取得したのにいつの間にか検索用Webサイトがリリースされていたりして何度も悲しい気持ちに。

**特に3回くらいクローラを作り直した北海道、鹿児島県、奈良県**については複雑な感情があります。(鹿はともかくその刺身はどこの海から取ってきたの？)
![sashime](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/bccc08b1-9e88-3fbf-8dce-f4a39a365b87.png)
2021年1月以降は修正も週1くらいの頻度に落ち着いてきているのでひとまず一安心ではありますが、ほとんどの都道府県ではキャンペーン期間が6月末まで伸びているので、まだ油断できません… [^5]

[^5]: リリース3日前にこの記事を書いててました。伏線です

# 各都道府県ごとにほぼ独自の仕様/実装

同じシステムを流用している一部自治体[^2]を除き、各都道府県でほぼ独自にWebサイトが開発されています。これはGoToEatの食事券発行委託事業が、[都道府県単位での入札となっているため](https://www.maff.go.jp/j/shokusan/gaisyoku/attach/pdf/hoseigoto-33.pdf)ではないかと推測します。
![連合艦隊感あって見てる分にはワクワクする](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/629a63ad-7ded-a596-4a8a-05fcad611cc3.png)

このためパッと見で見た目が似てても、内部実装はほとんど別物だったり、細かい部分の仕様(ジャンル分けとか検索仕様とか)が異なっていたりしました。

結果として予想以上にクローラの使いまわしができず、**40/47都道府県**くらいは個別に実装しています。

[^2]: LINY系(神奈川県、千葉県、滋賀県)とスパイラル系(茨城県、岐阜県、三重県)はほぼ同一の仕様だったので、かろうじて使いまわしができています。

# 検索ページを複数タブで開くとセッションが混ざる北海道

2020年に新規開設されたサイトでそんなことある…？

Scrapyでこの手のCookieベースのセッション管理(+csrf対策)に対応しようとすると意外とめんどくさく(ScrapyのリクエストはQueueに積まれて並列で走るので、エリア別でGET/POST検索クエリを発行しないとけない謎仕様とも相まって個別にCookieをstore/loadしないといけない)、2日くらい試行錯誤したあとに諦めてrequests + lxmlでクローラを実装しました。

# Excelから吐き出したHTMLで運用してる鹿児島県

つよい。ちなみにこうなる前まではPDFで、それはそれで地獄でした。罫線がないレイアウトだとPDFからテーブルとして認識ができないのでその辺を力技で…

![スクリーンショット 2021-02-08 22.23.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/395e5d96-c4da-0291-9929-f3ea29c7452a.png)


# 某グルメサイト上で運用してる上に件数が多い東京都

東京都の公式サイトでは2020/11/20にようやく店舗一覧が公開されたのですが、それが某グルメサイトの上にデータを置いただけのものでした。

しかも件数が33,000件以上と多く(大阪府の約2倍)、さらに詳細ページまで取得しないと住所データが取れない仕様。これはクローラのお作法である**1リクエスト/1秒を厳守しても8時間以上かかってしまう**計算でした。

叩きたくないなあと思いつつクローラを実装して動かしてみたのですが、取得したデータのどこまでが某グルメサイトの著作物にあたるのかがわからないため、対応はペンディング。[^3]

最終的には12月中旬にPDF形式での店舗一覧が提供されたので、これを元に[tabula-py](https://github.com/chezou/tabula-py)を使って店舗情報を抽出しています。
![こちとら元Tier4区民だぞ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/6faf1c13-2b66-c738-759b-07485ccec4db.png)

余談ですがこの区分、[JIS X0402](https://www.jisc.go.jp/app/jis/general/GnrJISNumberNameSearchList?toGnrJISStandardDetailList)という規格の定義順なんですね。~~都民のTier表として喧嘩を売ってるのかと思った…~~

[^3]: 店舗名や住所などは公知のものなので著作物にはあたらない認識ですが、ジャンル名あたりになると微妙かなと思っていました

# 形が微妙な千葉県

千葉県のサイトは以下のような仕様でAPIレスポンスを元に表示していますが、**野田のあたり(左上)と銚子のあたり(右端)が存在するせいで**余計な場所まで見ないといけないため、
無駄な時間と負荷をかけてしまっていました。
<img width="700px" alt="千葉くん" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/fa063514-adb4-7a19-3d7c-23e811dac6e2.png">
これは**都道府県メッシュ**を使い、メッシュ単位で処理することで解決しました。神奈川県、滋賀県についても同様に対応しています。
[都道府県別500mメッシュ \- 500mメッシュ\_12\_千葉県](https://www.geospatial.jp/ckan/dataset/npli-pref-500m/resource/35f29e75-1774-4c3f-9dde-9a4daf517a17?inner_span=True)
![チーバくんがデストロイモードになると野田市はどうなるのか](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/ab2c555c-3757-00f0-8b57-5ada0a694a6c.png)

# 一つの県に複数のGoToEatキャンペーン

**[静岡県と鹿児島県については複数のGoToEat食事券](https://gotoeat.maff.go.jp/#List_area1)**が存在しており、対応しているお店が異なります。
<img width="700px" alt="青と赤、別に西と東ってわけではなかった" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/ed576088-3a26-74f5-7512-4aabbd4c60be.png">
![ローカル女子の遠吠え(瀬戸口みづき)、おすすめです](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/b8d0f979-2a8e-8a51-1f9e-70ebf0635a90.png)
<img width="700px" alt="仲悪いの？" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/9245a20e-300e-9442-17d4-f89496326b99.png">
![静岡もそうなんだけど商工会と商工会議所って対立しがちなの？](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/4f00b6a9-9a2d-70b1-59dd-ed80efe737bd.png)
静岡県ではおおむねどちらの商品券も利用できるのですが、片方のキャンペーンにしか対応していないお店も、無視できない程度にはありました。
![微妙](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/b367f276-b3b4-a157-000c-3667fd6c775f.png)
データを寄せることも検討しましたが、**「住所や店舗名に表記ゆれがある」「電話番号でも誤入力や片方だけ入力されている場合がある」**ため、同一店舗と判定することが非常に難しく、諦めざるを得ませんでした。

結果として最初に実装していた[「ふじのくに静岡県GoToEatキャンペーン食事券」(通称・赤券)](https://premium-gift.jp/fujinokunigotoeat/)のみが本サービスの表示対象となっています。

[鹿児島県の商工会議所ベースの方](https://gotoeat-kagoshima.com/registered-store/)については件数が少なく(100件以下)、そもそも店舗住所が載ってないので対応不可でした。

# 料理ジャンルの選択肢の先頭にうどんを持ってくるうどん県

![スクリーンショット 2021-02-08 22.39.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/7a34ce66-91e4-ca06-2a21-f68172809e02.png)

# よくわからない勢いがある佐賀県

![スクリーンショット 2021-01-18 0.43.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/0b8e5dfd-aa41-bc04-851f-27c4ca45b776.png)

# Scrapyのログ出力周りが使いづらい

`scrapy`コマンドで叩いたときと、スクリプトから`CrawlerProcess`経由でぶん回したときとで挙動を切り替えるのに試行錯誤しました。

全部標準出力に出す方式だとGitHub Actionsのログが汚れまくるのでファイルに吐いて… 途中でコケたときに追うことも考えるとリポジトリにコミットして…とかやりだしたため、非常にめんどくさかった記憶があります。

今振り返れば[S3とかに吐いてしまう](https://doc-ja-scrapy.readthedocs.io/ja/latest/topics/media-pipeline.html#amazon-s3-storage)のが一番ラクだったのかなと。実際ほとんどログを見返すことってないですしね。(どうせ手元でRe-Runして再現するか試さないといけないのであんま意味なかった)

# Scrapyのリトライが微妙

リトライ回数(RETRY_TIMES)しか指定できないっぽい。しかたないのでMiddlewareで対応しました。
https://zenn.dev/terukizm/articles/d53a536809b101

# 1日5個クローラ書いても47都道府県で10人日かかる

算数ができていませんでした。ちなみに1日5個はかなりうまく行ったケースです。


# 公式サイトの入力データがとにかく汚い

* 住所がGoogleとかに登録されているのと違っている
* 店名もGoogleとかに登録されてるのとは違っている
* 郵便番号も違っている
* 電話番号も違っている

という感じで、何が正しいんだ…というのがよくありました。クローラの結果を差分で見ててもこんな感じで、毎日結構な頻度で修正が入ってます。(最近落ち着いてきたけど1日100件くらいは普通)

* 店名、そば処だもんね
![そば処だからね](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/8188d281-f505-6fb9-7cc5-38909f46bb74.png)
* 似てるもんね
![似てるよね](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/c191213f-9285-f6d9-0a82-815002484bdc.png)
* カフェではなくていいのか
![カフェではなくていいのか](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/aa39e5f7-3cc7-4e68-b43c-1e7c65e75e89.png)
* 山神仙人の方が気になるんですが
![山神仙人の方が気になるんですが](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/c988ec71-a089-6e04-bc73-156b527b17a8.png)
* Excelからデータ流し込んでるのかな…
![Excelからデータ流し込んでるのかな…](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/73c41b0c-3130-a0b2-162e-3566183a0321.png)
* それ表示していいデータ？
![非表示のデータ出ちゃってない？](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/7af3fb50-4d93-0c46-9198-3bc6ff8d5bae.png)
* よかった(うれしい)
![よかった](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/75508a29-6b03-d2b1-b053-b77ca7d3c02a.png)

# 公式サイトの入力データが重複

「店舗名」「住所」が同一の場合を重複として[^4]、

* クローラの実装不具合
* 普通に公式サイトに重複入力されてる
* [ソートキーがユニークでないのでページングしたときに結果が一意にならず重複してしまう](https://blog.mmmcorp.co.jp/blog/2018/03/22/sql_order_by/)

と3パターンあるため、確認作業が地味にしんどかったです。最後のはソートキーとしてOrder By にupdated_atだけを指定してたりすると初期データ投入した時にでよく起きるやつで、公式サイトに商工会議所経由で報告して直してもらったりしました。

めちゃくちゃ丁寧に返答いただいたりしてこちらこそありがとうございますという感じでした。

[^4]: 住所だけだとショッピングモールなどの場合に住所がかぶる、店名だけだとよくある店名の場合にかぶる(例: 愛知県には「すずや」が複数ある)など

# 住所にハイフンっぽいハイフンじゃない文字が混じってる

OCRとか変なIME(Androidのだと色々あるらしい)経由で入力されたのか、パッと見ではわかんないハイフンっぽいハイフンじゃない文字が色々入力されていたりして死にました。

最終的には`"|-|‐|－|‑|ー|−|‒|–|—|―|ｰ|"`みたいなことに。

参考: [ハイフンに似てる文字の文字コード \- Qiita](https://qiita.com/ryounagaoka/items/4cf5191d1a2763667add)

# 位置がズレてても土地勘ないとわからない

知らない場所だとプロットされたものを見てても位置ズレがまったくわかりません。

**「地方移住して土地勘がないのでWebサービスを作った」**はずなのに、結果として家の近くの飲食店の場所に、異常に詳しくなりました。

本末転倒ですね。


# 変な住所がある

GoToEatの対象が飲食店なので、変わった住所も結構ありました。

* スキー場なんかは国有林が多く、番地なしが結構あるっぽい
![説明サンキュー...](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/8adcb9d9-ae01-e686-73b6-2094781af8c7.png)

* そういや屋台って住所どうしてるんだろう…
![屋台、そういうのもあるのか](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/110bc203-3965-7ae5-fd60-9a31eb6f949f.png)

* 農道沿い…
![農道沿い…](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/101888cc-4358-6ec3-7bf6-fc43a91de436.png)

* 全体的に情報量が多い
![未来感あるよな](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/fe810a0d-c5df-5e7b-1a5a-834c3a6f1159.png)

# DAMSのジオコーディング精度が出ない

DAMSで内部的に使われてる位置参照情報データの仕様上どうしようもなく、DAMS自体も「もっと細かいのが必要なら商用データとかを加工して自分で辞書データ作ってね(その割にはドキュメントとかはほとんどないけど)」っていうスタンスです。

ただ特定都道府県の公式サイトはGoogleMapへプロットするためにlatlng情報を保持しているため、これをこっそり取得して使うことで、精度を出したりしています。

どことは言いませんが、`<script>`タグ中に**js配列**(jsonじゃなくて)をベタにdumpしているので、それを[demjson](https://pypi.org/project/demjson/)でparseして読み込む…といった邪悪な処理があったりします。

![スクリーンショット 2021-02-09 15.18.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/f7812e81-fbee-98b9-c4b0-887404e2571d.png)



# ジャンル名が自由入力

岩手県と東京都は申請者の自由入力をそのまま使ってるのか本当にカオス。
![スクリーンショット 2021-02-07 11.03.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/cdadabac-f07e-f168-9972-5ad7363e7e83.png)
**「イランリョウリペルシャリョウリジャーメジャムアサガヤテン」**というジャンル名に至っては「異国での申請、大変だったんだろうなあ…」という感想しか出ませんでした。
![杉並区のイランリョウリペルシャリョウリジャーメジャムアサガヤテン屋さん、頑張ってほしい](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/55790d7d-75e4-571d-ef0d-c41aa82f4927.png)

東京都はあまりにフリーダムすぎて途中で諦めました…　そもそもなんの店なのか普通にわからない店とかある…


# GitHub ActionsでGitベースのワークフロー(成果物をリポジトリに放り込むタイプ)を組むと、エラー時のハンドリングがしんどい

**クローラはいろんな理由で急にコケます**。(サイト更新でDOMが崩れる、想定外のデータが投入された、深夜のサーバメンテナンスに出くわす、突然50xが帰ってくるなど)

そういったエラー時の対応、「クローラでは正常終了した成果物はコミットしておきたいが、異常終了した成果物については破棄したい。でもログについては確認できるようにして、再実行する場合は移住終了したクローラだけ実行できるようにしたい」というふうに、**細かい運用を考え出すと仕組みが複雑になりやすい**です。

他にも、基本的には対話ベースであるGitコマンドを、ファイルが存在しない場合でも問題なく処理できるように標準エラーを握りつぶしたり、変な差分が出ないように成果物をソートしておいたり、GitHubでプレビューできないような巨大なdiffが発生しないようにコミット単位を調整したり…といった、わりとセンシティブな作り込みが必要になりました。
![もっとうまい方法があったのかもしれない](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/06e151f9-49b1-66b5-e3da-dc9c47ac598e.png)
成果物をリポジトリにコミットしてPRで処理結果を確認する仕組みは、**きちんと正常系で動いてれば非常に楽**で、触ってて気持ちが良く、変更履歴も残るのでいい感じではあるのですが、構築に手間がかかり、エラー時の対応がブランチワークを駆使した複雑な(属人性の高い)ものになりやすいので、**一概にこれがベストとは言えない**なあと思いました。

特に私の場合は、GoToEat公式サイトが安定しきっていない時期と作り込みのタイミングが重なったため、**「GitHub Actionsでワークフローを組み立てつつ、クローラがコケてれば公式サイトを確認してクローラを修正し、ジオコーディング結果を見ながら住所に対する正規化処理を調整」**というのを2週間くらい延々とやる羽目になり、とてもつらい気持ちになりました。

今思えばクローラについては0円運用にこだわらず、適当な省電力PCを常時起動させておいて、適当に定期実行させる方が圧倒的にコスパが良かったのかなと思います。失敗した場合のRe-Runとかもキャッシュから取れますしね。

# GitHub Actionsのスケジュール実行が1時間くらいブレる

深夜帯はスタックしがちなのか、cron指定した時間から1時間くらい遅れて起動したりします。あんまり遅すぎると朝までに終わんなかったりするので、開始時間の調整がめんどくさかったです。
![スクリーンショット 2021-02-09 15.11.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/2a8299d8-d7ce-f2f3-53ac-e06c65839347.png)
あとUTC指定の逆算。


# alpine.jsからVue.jsへの移行が意外とめんどう

参考にしたTailwindCSSのコンポーネント実装サンプルに従って、TailwindCSS + [alpine.js](https://github.com/alpinejs/alpine)でデザインモックに動きのある実装をしてからVue.jsに移行していました。

Vue.jsと文法は似てるんですが、そこまで無加工で移行できるってわけでもなかったので(特にx-dataとかの書き換えがだるい)、モックではデザイン面でTailwindCSSを使うだけにして、素直にVue.jsとかSPAの方で実装したほうが楽かなと思いました。

めちゃめちゃ複雑な動きを実装したい、とかならまた別なのかもしれませんが…

# 食い物のアイコンを探すのが難しい

カフェとかレストランくらいならそこら中にあるんですが、そこから細かい単位(焼肉とか中華とかカレーとか)になるとなかなかなくて困りました。

最終的には以下のサイトからお借りしております。

* [ICOOON MONO](https://icooon-mono.com/)

豊富でクオリティが高く、おすすめです。
![使い道が謎](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50184/8292cf31-ab82-fcd1-3f3a-49a5d6c0bed4.png)

# Mapbox GL JS

むっかしかったです。処理の依存関係がよくわかんなくて、読み込み終わんないタイミングで触ってコケるとかよくありました。終盤はなんとかなってきたので最終的には慣れだと思いますが。


# pyppeteerがデフォルトで使ってるChromiumのバージョンが古い(71.0.3542.0)

大分県のサイトが更新され、依存ライブラリかなんかが[globalThis()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/globalThis)を使うようになったことにより突然死するようになりました。リリース直前に。

[PYPPETEER_CHROMIUM_REVISION](https://pyppeteer.github.io/pyppeteer/reference.html?highlight=reference#environment-variables)を指定してそれなりに新しいバージョンのChroniumで動かすようにして解決しましたが、全体的にpyppeteer、正直まだ人柱感あるなって思いました…



# (2021/02/12追記)リリース予定日直前にサイト更新連打

東京、大分、奈良、大阪と今まで苦しめられた自治体の波状攻撃が、よりによってリリース予定日の直前に来ました。ロックマンのラストステージかな？

リリース日を私の誕生日にあわせたかいがありました。本当にありがとうございます。

…んなわけねーだろ!!!!!!!!!!!!!!!!! [^7]

[^7]: まあ冷静にカレンダーの並びみたら、そりゃここの深夜でリリースぶっこみますよね。わかる。

# おわりに

ここまでどうでもいい内容をお読みいただきまことにありがとうございました。楽しんでいただけたのでしたら幸いです。

あとGoogle Geocoding API周りとかでもいろいろ酷いネタがあったりするので、そっちも気が向いたらまとめてみたいなと思っています。