Selenium + Heroku + LineAPIで、ふもとっぱらキャンプ場に空きが出たら通知する

f:id:ysdyt:20211008095839p:plain

selenium + heroku + Line APIで、ふもとっぱらキャンプ場予約サイトを10分毎にスクレイピングし、予約に空きがでたらLineに通知するというアプリ、を作った人がいるのでそれを動かすために試行錯誤した話のメモです。

これで予約戦争に勝つる...!

予約がとれない聖地

キャンパーの聖地とも言われるふもとっぱらキャンプ場は、3ヶ月先までのキャンプ宿泊予約を専用のwebサイトでネット予約できますが、あまりの人気のため公開されるやいなや3ヶ月先まで基本的に土曜日は常に予約が埋まっています。

fumotoppara.secure.force.com

この状況となる人気以外の原因の一つとして、予約を無視して行かなかったとしてもペナルティーがない(予約時にクレカを登録したりせず支払いは現地だから)というのもあります。「とりあえず予約だけしとくか」「いけなくなったけどわざわざキャンセルしなくてもいっか」が実際は通ってしまう運用になっている点です。 キャンプ場もこのあたりまで厳密に運営する元気は無いのでしかたないところです。

ということで予約画面とにらめっこするのは大変なので自動化しようとしたら、やはり先人がおられました。

上記のコードをありがたくforkし、自分の環境でも動くようにしたものがこちら。 github.com

動かすまでの確認作業の大まかな流れは以下です。

1. seleniumで正しくスクレイピングできるか確認(ローカル)
2. Line Message APIが正しく動くか確認(ローカル)
3. Herokuに移植して、heroku上でも正しく動くか確認(heroku)

このブログでは、folkしたコードを使って自分のherokuで動かすまでに手こずったところをメモとして残しておきます。

元ネタのブログでも詰まったところについて触れられていますが、主なハマりポイントは以下です。

- seleniumを動かす方法
 - google chromeとchrome driverの設定方法
- herokuの動かし方
 - herokuでのgoogle chromeとchrome driverの設定方法
 - LINEのtokenをどうやって設定するのか

おおよその解決策に対する参考になるブログ記事はネタ元のブログにも紹介されているのでそちらを先にざっと読むとわかりやすいです。ここではより細かいトラブルシューティングのメモとなります。

まずはseleniumをローカルで動かす

forkしてきたコードの処理内容を把握するためにまずローカルで動かしてみようとしました。

qiita.com

selenium公式がdocker imageを作ってくれていますが、最初は自分で環境作ってみたいなと思いminicondaの上に作ることにしました。

seleniumはブラウザの自動操作を行うものなので、実際にブラウザが必要になります。ここではgoogle chromeを使いました。現在使っているgoogle chomeのバージョンを確認すると、自分の環境では バージョン: 94.0.4606.71(Official Build)(x86_64)でした。(確認方法は google chromeの「︙」>「設定」>「Chromeについて」)

次に、seleniumからchromeを動かすために必要なchromedriverというものをpip installしておく必要があります。注意点として、chromedriverはgoogle chromeのバージョンに合ったものでないといけず、現状の自分のバージョンと見比べて指定しないといけません。ややこしいのは、google chromeと同じバージョンを指定するのではなく、以下のページを見て、一番数字が近い、低いバージョンを指定しないといけない点です。

ChromeDriver - WebDriver for Chrome - Downloads

ここでは、google chromeのバージョンが94.0.4606.71であったため、一番近くて低い94.0.4606.61.0を指定します。

pip install selenium
pip install chromedriver-binary==94.0.4606.61.0

実際に呼び出して使ってみます。試しに、googleにアクセスして「selenium」と検索する動作を行ってみます。上手く動けば、コードを実行するとgoogle chromeが勝手に立ち上がって指定の動作をするはずです(最初に見たときはちょっと感動)

import time
import chromedriver_binary 
from selenium import webdriver

driver = webdriver.Chrome()
driver.get('https://www.google.com/')
time.sleep(5)
search_box = driver.find_element_by_name("q")
search_box.send_keys('selenium')
search_box.submit()
time.sleep(5)
driver.quit()

chromedriverをダウンロードする必要があることと、どのバージョンをダウンロードしないといけないかがちょっとわかりにくいくらいです。

heroku スケジューラーの設定

今回のタスクは常時処理が走っている状態ではなく、定期的にスポットで処理が走ってくれたらokです。 herokuではその場合はスケジューラー機能を使えば実現できます。

設定方法は以下です。(無料枠内の使用であってもスケジューラー機能の使用にはクレカの登録が必要)

qiita.com

今回はスケジューラーの最小起動頻度である10minごとを指定し、動かしたいタスクとしてpython3 notification.pyを設定します。dynoサイズも小さい方であるStandard-1Xを選んでおけば良さそうです。

f:id:ysdyt:20211007233014p:plain

ここでは管理画面からGUIで設定しましたが、こちらに書いてあるとおりコマンドラインからも同様の設定ができるようです。

herokuの無料枠

Free dyno 時間 | Heroku Dev Center

クレカ認証を行うと毎月1000時間のFree Dynoを得られます。10分に一回コードを起動するタスクがどれくらいのdynoを消費するのか厳密にはわかりませんが、実際に以下コマンドで消費量を確認したところ微々たるものっぽいです。今回のようなアプリでは1%分消費できるかどうかという感じで完全に無料枠内に収まります。

$ heroku ps -a fumotoppara-yoyaku
Free dyno hours quota remaining this month: 996h 29m (99%)
Free dyno usage for this app: 2h 57m (0%)
For more information on dyno sleeping and how to upgrade, see:
https://devcenter.heroku.com/articles/dyno-sleeping

No dynos on ⬢ fumotoppara-yoyaku

herokuでのTokenの設定

TokenやAPI key, パスワードのように内緒にしておきたい情報の取り扱い方です。

herokuに関係なく、通常そういった情報が含まれるコードをgithubなどで管理したい場合は、以下のように隠しファイルに情報を入れておき、gitignoreでリモートにプッシュしないようにしておくなどの方法があります。

# .envファイルを作り、そこにLINE tokenを記載する
$ touch .env
$ echo LINE_TOKEN='xxxxx' >> .env
# dotenvモジュールでファイルから情報を抜き出す
import os
from dotenv import load_dotenv #pip install python-dotenv で入る

dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(dotenv_path)
LINE_TOKEN = os.environ.get("LINE_TOKEN")

herokuではgitと同じ方式でファイルを管理・pushすることでアプリをdeployしますが、同じ様にするとherokuからは .envファイルが見えなくなりtokenが取得できなくなります。

そこで、herokuで同じことをするためには、herokuの環境変数に登録することで実現します。方法は以下。

cream-kuchen.hatenablog.com

今回だとこんな感じです。(chromedriver側の話は後述)

f:id:ysdyt:20211008091852p:plain

これもGUIでも出来るしコマンドでもできます

$ heroku config:set LINE_TOKEN="hogefugapiyo"

登録された変数は heroku configコマンドで確認できます。

$ heroku config
=== fumotoppara-yoyaku Config Vars
LINE_TOKEN:  hogefugapiyo

herokuでのChromeとdriverの設定

今回もっともハマったところです。heroku上でgoogle chromeを操作してseleniumを動かしたい場合、herokuのSettingsでBuildpacksを指定しないといけません。

qiita.com

f:id:ysdyt:20211007233612p:plain

これもGUIでもできますしコマンドでもできます

$ heroku buildpacks:set https://github.com/heroku/heroku-buildpack-chromedriver.git -a fumotoppara-yoyaku
$ heroku buildpacks:set https://github.com/heroku/heroku-buildpack-google-chrome.git -a fumotoppara-yoyaku

設定後、git push heroku mainすると上記に登録したものが実際にダウンロードされてheroku上にbuild&deployされるため、設定後は空pushでもいいのでpushを忘れないように。

そしてさらに、多くの人がハマるのがおそらく次で、実際にここまで設定してherokuにpushしても、何らかの理由でpushがコケるという状態が発生しがちです。自分の場合は以下のようにunzipエラーを吐かれました。

remote: Archive:  /tmp/chromedriver.zip
remote:   End-of-central-directory signature not found.  Either this file is not
remote:   a zipfile, or it constitutes one disk of a multi-part archive.  In the
remote:   latter case the central directory and zipfile comment will be found on
remote:   the last disk(s) of this archive.
remote: unzip:  cannot find zipfile directory in one of /tmp/chromedriver.zip or
remote:         /tmp/chromedriver.zip.zip, and cannot find /tmp/chromedriver.zip.ZIP, period.
remote:  !     Push rejected, failed to compile chromedriver app.
remote: 
remote:  !     Push failed

理由の多くの場合は「Chromeとdriverのバージョンが一致していない」ことなどです。
これを一致させてpushしてもコケさせないようにするには、Config Varsに設定するCHROMEDRIVER_VERSIONのバージョン数を以下のように調べて指定する必要があります。

cream-kuchen.hatenablog.com

自分の環境での例です。まずbuildpackでダウンロードされたchromeのバージョンを確認。

$ heroku run google-chrome --version -a fumotoppara-yoyaku
Running google-chrome --version on ⬢ fumotoppara-yoyaku... up, run.4315 (Free)
Google Chrome 94.0.4606.71 unknown

94.0.4606.71に一番近くて低いchrome driverのバージョンをこちらで探すと、ChromeDriver 94.0.4606.61とわかったのでConfig Varsに登録する

f:id:ysdyt:20211008091852p:plain

コマンドラインで登録する場合は

$ heroku config:set -a fumotoppara-yoyaku CHROMEDRIVER_VERSION="94.0.4606.61"

これで再度git push heroku mainします。晴れてpushが成功するとActivity画面にBuild succeededのログが残ります。

f:id:ysdyt:20211003205908p:plain
pushに成功するとBuild succeededとなる(失敗するとBuild failed)

実際にダウンロード・ビルドされたchromedriverのバージョンを見てみると、たしかに指定したものになっていることがわかります。

$ heroku run chromedriver --version -a fumotoppara-yoyaku
Running chromedriver --version on ⬢ fumotoppara-yoyaku... up, run.4744 (Free)
ChromeDriver 94.0.4606.61 (418b78f5838ed0b1c69bb4e51ea0252171854915-refs/branch-heads/4606@{#1204})

正しく動いているかログを確認する

herokuへのpushが成功した後、ここでは10分に一度指定の.pyを叩くようにschedularに指定していたので、それが正しく動き続けるか確認します。 確認は$ heroku logs --tailコマンド。

Configuration and Config Vars | Heroku Dev Center

もしもdeployがコケていたり、何かしらコードにバグがあればここのエラー文を見るとなんとなくわかるはずです。
意図通り動いている自分の環境では、以下のようにログが出力されています。

2021-10-07T14:39:49.532222+00:00 app[api]: Starting process with command `python3 notification.py` by user scheduler@addons.heroku.com
2021-10-07T14:40:03.126384+00:00 heroku[scheduler.2676]: Starting process with command `python3 notification.py`
2021-10-07T14:40:03.722456+00:00 heroku[scheduler.2676]: State changed from starting to up
2021-10-07T14:40:04.234487+00:00 app[scheduler.2676]: /app/.heroku/python/lib/python3.9/site-packages/selenium/webdriver/firefox/firefox_profile.py:208: SyntaxWarning: "is" with a literal. Did you mean "=="?
2021-10-07T14:40:04.234504+00:00 app[scheduler.2676]: if setting is None or setting is '':
2021-10-07T14:40:15.204707+00:00 heroku[scheduler.2676]: Process exited with status 0
2021-10-07T14:40:15.293579+00:00 heroku[scheduler.2676]: State changed from up to complete

Starting process ~~~ から始まり、指示通り処理を終えられたら State changed from up to complete で終わる感じです。(途中にfirefoxブラウザに関するsyntax warningが出ていますが、動いているし気にしないことにする)

herokuでの動作確認

今回のアプリの場合、上記までで上手くdeployができれば、放っておくと10分に1度Lineにキャンプ場空きメッセージが届くようになるはずです。

ただ、10分待つのも面倒なので、すぐに指定の処理を動かして正しく動くかどうか確認したい場合は、以下のようにheroku run で実行が可能です。

$ heroku run python3 notification.py

ここまでやっても予約が取れねぇ

1週間ほど動かしてみたところ、土曜日の空き通知が10件ほど来ました。思ったよりはキャンセル申請をちゃんとする人がいるようです。しかし結果としてはことごとく予約を取れませんでした(なんでや)
通知がきてソッコーで予約ページを見にいってもすでに埋まっている(!)とか、入力している間に誰かが予約を完了してしまって取れなかった、などです。

herokuスケジューラーの最小起動頻度が10分に1回のため、通知が来てすぐに見に行ったとしても最大で10分間空き状態が存在していたことになるため、その間に取られるようです。(ここは運ゲーですね。)
逆にいうと空きが発生しても10以内でほぼ確実に埋まっているため、手動監視している人はよほどの強運が無いと偶然キャンセルを見つけて予約をゲットできる可能性はかなり低そうです。

たくさんの人がプログラムで監視してるとは思えないですが、それにしても埋まるのが早すぎる。。。スクレイピング勢が予約入力フォームも自動で打ち込ませてるのではと思う速さです。ふもとっぱらキャンプ場の予約、実はデジタル戦争なのか?

追記

その後、無事に予約ゲットできました!10分ですぐに埋まらず、なぜか20分たっても30分たっても予約が埋まりきらない土曜日も極たまにはあるようです。