Selenium + Heroku + LineAPIで、ふもとっぱらキャンプ場に空きが出たら通知する
selenium + heroku + Line APIで、ふもとっぱらキャンプ場予約サイトを10分毎にスクレイピングし、予約に空きがでたらLineに通知するというアプリ、を作った人がいるのでそれを動かすために試行錯誤した話のメモです。
これで予約戦争に勝つる...!
予約がとれない聖地
キャンパーの聖地とも言われるふもとっぱらキャンプ場は、3ヶ月先までのキャンプ宿泊予約を専用のwebサイトでネット予約できますが、あまりの人気のため公開されるやいなや3ヶ月先まで基本的に土曜日は常に予約が埋まっています。
この状況となる人気以外の原因の一つとして、予約を無視して行かなかったとしてもペナルティーがない(予約時にクレカを登録したりせず支払いは現地だから)というのもあります。「とりあえず予約だけしとくか」「いけなくなったけどわざわざキャンセルしなくてもいっか」が実際は通ってしまう運用になっている点です。 キャンプ場もこのあたりまで厳密に運営する元気は無いのでしかたないところです。
ということで予約画面とにらめっこするのは大変なので自動化しようとしたら、やはり先人がおられました。
自分もちょうど同じ悩みに直面していたので、つくってnoteにまとめました!ご参考まで😺https://t.co/kASjIeY8yt
— rinascimento (@rinascimento741) 2021年9月27日
上記のコードをありがたく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してきたコードの処理内容を把握するためにまずローカルで動かしてみようとしました。
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ではその場合はスケジューラー機能を使えば実現できます。
設定方法は以下です。(無料枠内の使用であってもスケジューラー機能の使用にはクレカの登録が必要)
今回はスケジューラーの最小起動頻度である10minごとを指定し、動かしたいタスクとしてpython3 notification.py
を設定します。dynoサイズも小さい方であるStandard-1X
を選んでおけば良さそうです。
ここでは管理画面から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の環境変数に登録することで実現します。方法は以下。
今回だとこんな感じです。(chromedriver側の話は後述)
これも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を指定しないといけません。
これも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
のバージョン数を以下のように調べて指定する必要があります。
自分の環境での例です。まず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
に登録する
コマンドラインで登録する場合は
$ heroku config:set -a fumotoppara-yoyaku CHROMEDRIVER_VERSION="94.0.4606.61"
これで再度git push heroku main
します。晴れてpushが成功するとActivity
画面にBuild succeeded
のログが残ります。
実際にダウンロード・ビルドされた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分たっても予約が埋まりきらない土曜日も極たまにはあるようです。