某ワクチン予約サイトで空きがあるかどうかをPlaywrightで定期的に調べる
こういうのをポチポチやって空きがあるかどうか調べるのは、とても面倒だし自動化したい。
Playwrightのインストール
今回は自前のRubyクライアントでやってみる。
Playwrightサーバーのインストールと起動
$ npm install playwright@next
$ ./node_modules/.bin/playwright install
$ ./node_modules/.bin/playwright run-server 8000
Listening on ws://127.0.0.1:8000/ws
Playwrightクライアントのインストール
$ bundle init
source "https://rubygems.org"
gem 'websocket-driver'
gem 'playwright-ruby-client', '1.14.beta3'
gem 'pry-byebug'
$ bundle install
ログイン状態にする
ログイン画面で接種券番号&パスワードを書く方法もあるけど、いちいちログインするのは無駄なので、
↓に書いてあるようにクッキー&ローカルストレージをJSONエクスポートして、ログイン状態で操作開始できるようにする。
ワクチン予約サイトに入ってPauseするだけのスクリプトをまずは組む。
require 'playwright'
Playwright.connect_to_playwright_server('ws://127.0.0.1:8000/ws') do |playwright|
playwright.chromium.launch(headless: false, channel: 'chrome-canary') do |browser|
page = browser.new_page
page.goto('https://vaccines.sciseed.jp/fukuokacity/')
require 'pry'
binding.pry
page.context.storage_state(path: 'mystate.json')
end
end
$ bundle exec ruby main.rb
ここからは、手動でログイン
ログイン後の状態はクッキーかローカルストレージあたりに保持られているので、
[1] pry(main)> exit
で
page.context.storage_state(path: 'mystate.json')
を通す。
$ cat mystate.json
でそれっぽいログイントークンが入ってることを確認。
playwright.chromium.launch(headless: false, channel: 'chrome-canary') do |browser|
page = browser.new_page(storageState: 'mystate.json')
page.goto('https://vaccines.sciseed.jp/fukuokacity/')
require 'pry'
binding.pry
end
しばらくは、これで、ログイン状態でブラウジングできる。
日付選択
https://vaccines.sciseed.jp/fukuokacity/reservation-date に直アクセスしても、トップに戻されるので、このへんは、地味に頑張るしかない。
page = browser.new_page(storageState: 'mystate.json')
page.goto('https://vaccines.sciseed.jp/fukuokacity/reservation-date')
page.click('text=新規予約')
page.click('text=日付から探す')
page.click('.s-selector') # 摂取ワクチンを選択してください
page.click('text=ファイザー社ワクチン')
page.click('.page-search-reservation-frame-from-date_date') # 日付
datepicker = page.query_selector('.page-search-reservation-frame-from-date_datepicker')
page.wait_for_selector('.cell.day')
selectable_days = datepicker.query_selector_all('.cell.day:not(.disabled)').map(&:text_content)
datepicker.wait_for_selector("text=#{selectable_days.first}").click
page.check('input[type="checkbox"]') # 予約可能な会場のみ表示
selectable_days.each do |day|
page.click('.page-search-reservation-frame-from-date_date') # 日付
datepicker = page.query_selector('.page-search-reservation-frame-from-date_datepicker')
page.wait_for_selector('.cell.day')
datepicker.wait_for_selector(".cell.day >> text=#{day}").click
# API通信の完了を待つ。
# ただし、最初の1つは現在選択中なのでAjax request/responseされない。
page.expect_event('response') unless day == selectable_days.first
items = page.query_selector_all('.department-list-item')
next if items.empty?
puts "#{day}日:"
require 'pry'
binding.pry
end
空きがあればbinding.pryで止まるはずなので、「はい、一個も空いてませんね」というのは一瞬でわかる。
まとめ表示
puts "#{day}日:"
items.each do |item|
name = item.query_selector('.department-list-item_name').text_content.strip
station = item.query_selector('.department-list-item_nearest-station').text_content.strip
area = item.query_selector('.department-list-item_area').text_content.strip
puts "--> #{name} (#{station}/#{area})"
end
こんな感じにすれば、空きがあるときには
$ bundle exec ruby main.rb
16日:
--> もりはら内科クリニック (山王一丁目バス停下車(徒歩2分), 博多駅, 竹下駅/博多区)
29日:
--> ゼロマチクリニック天神 (西鉄福岡(天神)駅/天神)
こんな感じで出る。
空きが生じたら/なくなったら通知する
自分用の通知なので、手軽に使えるLine Notifyを使う。
Authorization: Bearer [トークン]
を指定して、 message=ほげほげ を https://notify-api.line.me/api/notify に向かってフォームPOSTするだけなので、かなりお手軽。
require 'net/https'
require 'uri'
def line_notify(api_token:, text:)
req = Net::HTTP::Post.new('/api/notify')
req['Authorization'] = "Bearer #{api_token}"
req['Content-Type'] = 'application/json'
req.set_form_data({ message: text })
Net::HTTP.start('notify-api.line.me', 443, use_ssl: true) do |http|
http.request(req)
end
end
あとは、スクレイピングのたびに通知されるのは煩わしいので、前回からの変化があるときだけ通知を出すようにする。雑に、現在の状態をJSONダンプ/ロードするロジックを追加すればいい。
available_days = []
selectable_days.each do |day|
page.click('.page-search-reservation-frame-from-date_date') # 日付
datepicker = page.query_selector('.page-search-reservation-frame-from-date_datepicker')
page.wait_for_selector('.cell.day')
datepicker.wait_for_selector(".cell.day >> text=#{day}").click
# API通信の完了を待つ。
# ただし、最初の1つは現在選択中なのでAjax request/responseされない。
page.expect_event('response') unless day == selectable_days.first
items = page.query_selector_all('.department-list-item')
next if items.empty?
available_day_item = [
"#{day}日",
items.map do |item|
name = item.query_selector('.department-list-item_name').text_content.strip
station = item.query_selector('.department-list-item_nearest-station').text_content.strip
area = item.query_selector('.department-list-item_area').text_content.strip
"#{name} (#{station}/#{area})"
end
]
available_days << available_day_item
end
previous = JSON.parse(File.read('available_days.json')) rescue []
File.open('available_days.json', 'w') { |f| f.write(JSON.dump(available_days)) }
puts "previous=#{previous}, current=#{available_days}"
if available_days.empty?
# 予約可能な施設なし
if previous.empty?
# ないまま
else
line_notify('予約可能施設がなくなりました')
end
else
if previous.empty?
# 予約可能施設が出た
line_notify("予約可能施設があります\n#{line_dump(available_days)}")
else
# 予約可能なまま
# TODO(previousとcurrentを比較して、有用な情報は通知するのがよい)
end
end
line_dump
は予約可能状況をテキスト化するメソッド。自分の場合は何の工夫もなく、ただ垂れ流しにしている。
def line_dump(available_days)
lines = []
available_days.each do |day, items|
lines << day
items.each do |item|
lines << " - #{item}"
end
end
lines.join("\n")
end
これで、LINE通知がくる
あとは、適当なサーバーにアップロードして、認証情報である mystate.json
を置いて定期実行のためのcronを仕掛ければおk。