Closed4

某ワクチン予約サイトで空きがあるかどうかをPlaywrightで定期的に調べる

Yusuke IwakiYusuke Iwaki

こういうのをポチポチやって空きがあるかどうか調べるのは、とても面倒だし自動化したい。

Yusuke IwakiYusuke Iwaki

Playwrightのインストール

今回は自前のRubyクライアントでやってみる。
https://github.com/YusukeIwaki/playwright-ruby-client

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
Gemfile
source "https://rubygems.org"

gem 'websocket-driver'
gem 'playwright-ruby-client', '1.14.beta3'

gem 'pry-byebug'
$ bundle install

ログイン状態にする

ログイン画面で接種券番号&パスワードを書く方法もあるけど、いちいちログインするのは無駄なので、
↓に書いてあるようにクッキー&ローカルストレージをJSONエクスポートして、ログイン状態で操作開始できるようにする。
https://playwright.dev/docs/auth/#reuse-authentication-state

ワクチン予約サイトに入ってPauseするだけのスクリプトをまずは組む。

main.rb
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:
--> ゼロマチクリニック天神 (西鉄福岡(天神)/天神)

こんな感じで出る。

Yusuke IwakiYusuke Iwaki

空きが生じたら/なくなったら通知する

自分用の通知なので、手軽に使えるLine Notifyを使う。
https://notify-bot.line.me/doc/ja/

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通知がくる

Yusuke IwakiYusuke Iwaki

あとは、適当なサーバーにアップロードして、認証情報である mystate.json を置いて定期実行のためのcronを仕掛ければおk。

このスクラップは2021/08/13にクローズされました