この記事は
J:COMのテレビ番組表をselenium-webdriverでスクレイピングして、当日のヤクルト戦の番組内容をLineNotifyで知らせるスクリプトをherokuにデプロイ、1日1回定期実行させるまでのお話。
ただの趣味グラマの書きなぐり&二番煎じどころじゃないので参考にならないと思います。
経緯
やきう(ヤクルト戦)見よ
↓
J:COMのリモコン使いづら!
チャンネル調べるのメンドクサ…
誰か今日のチャンネル教えてくれんかなー
↓
ほー、LineNotifyなんてもんがあるのか
勉強がてらなんか作ってみるか
↓
毎日12:00にJ:COM番組表から取得した放送日時とチャンネルがLineで送られてきた!
やったね!
問題発生
4/23 12:00
あれ…?Lineから通知来たけど本文入ってないやん
とりあえずJ:COMのHP見てみる。
リニューアルか…
しゃーない、作り直すか。
そんなかからんやろ…
スクレイピングする
ruby + Nokogiri
この組み合わせの技術記事はネット上に(Qiitaにも)大量にあるし、自分自身も色々参考にさせてもらった。
Ruby + Nokogiriでスクレイピング
Ruby製の構文解析ツール、Nokogiriの使い方 with Xpath
スクレイピングのためのNokogiri利用メモ
もともとの環境
windows 10 Home
$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x64-mingw32]
$ nokogiri -v
Nokogiri (1.8.5)
スクレイピング部分
require "open-uri"
require "nokogiri"
url = "https://tvguide.myjcom.jp/search/event/?keyword=[%E7%94%9F]%20%E3%83%A4%E3%82%AF%E3%83%AB%E3%83%88&keywordTarget=title&channelType=120&channel=147_65406%2C219_65406%2C146_65406%2C166_65406%2C168_65406%2C169_65406%2C158_65406%2C132_65534%2C176_65406%2C138_65406"
charset = nil
html = open(url) do |f|
charset = f.charset
f.read
end
doc = Nokogiri::HTML.parse(html, nil, charset)
detail = doc.css("div.program_list:nth-child(2) div.detail")
ttl = detail.css("p.ttl").text
date = detail.css("p.date").text
chttl = detail.css("p.chttl").text
puts ttl
puts date
puts chttl
とりあえず動作確認。これでいけるだろ(ハナホジー)
$ ruby jcom_nokogiri.rb
""
""
""
Oh…なぜだ…
デベロッパーツールで確認しても特におかしなところは見当たらないんだがー。
調べた
どうやら動的ページはNokogiriではダメらしい。記事があった。
RubyでSeleniumを使ってスクレイピング
JavaScript等を用いた動的ページをスクレイピングするためのライブラリSeleniumのRubyでの使い方
ふむ、このseleniumとやらを使えばイケるのか。
ruby + selenium-webdriver
導入なんかはリンク先の通り。selenium-webdriverとChromeDriverを入れる。
ChromeDriverはpathを通しておく。
改良したらこんな感じになった。
require "selenium-webdriver"
caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {args: ["--headless"]})
driver = Selenium::WebDriver.for :chrome, desired_capabilities: caps
wait = Selenium::WebDriver::Wait.new(timeout: 10)
driver.get "https://tvguide.myjcom.jp/search/event/?keyword=[%E7%94%9F]%20%E3%83%A4%E3%82%AF%E3%83%AB%E3%83%88&keywordTarget=title&channelType=120&channel=147_65406%2C219_65406%2C146_65406%2C166_65406%2C168_65406%2C169_65406%2C158_65406%2C132_65534%2C176_65406%2C138_65406"
wait.until { driver.find_element(:css, 'div.program_list:nth-child(2) div.detail').displayed? }
detail = driver.find_element(:css, 'div.program_list:nth-child(2) div.detail')
ttl = detail.find_element(:css, 'p.ttl').text
date = detail.find_element(:css, 'p.date').text
chttl = detail.find_element(:css, 'p.chttl').text
puts ttl
puts date
puts chttl
-
--headless
オプションをつけるとchromeがGUI起動しないので今回の用途だと必要なし。 -
wait.until{}
は指定した要素が取得されるまで待ってくれるらしい。
seleniumで要素を待つ時にsleepを使うのはオススメしない
使用感はNokogiriとさほど変わらないと思った。Nokogiriが使えるならこれも使えると思う。
ただ、Nokogiriに比べて動作がもっさり。この辺は割り切るしかない。
あとサイトへの接続がうまく行かなかったりすると要素が取得できずタイムアウトして終了するので、適宜retry
などで再接続したら安定する。
LineNotifyとHerokuで1日1回定期実行
LineNotify
正直http周りはまっっっったくと言っていいほどわかりません。苦手意識があるのでしっかり学びたいところ。
なのでLineNotifyに関してはこちらの記事をパクリ参考にさせていただきました…
[Ruby] Line NotifyでLineにメッセージ送信する手順メモ
詳しく知りたいならこの辺をしっかり読めばいいんだと思います。
LINE Notify API Document
日本語なので英語嫌いでも安心!
Heroku
Herokuに関してもQiitaだけでも溢れるほど記事があります。私なんかは職業プログラマでもなんでもないので、必要知識は基本すぐパクります。無から有を生み出すレベルにないので。
参考にしたのはこちら
Herokuで単純なrubyスクリプトを定期的に実行する
今回はこれだけでは足りないのでこちらを参考に
HerokuにChromeとChrome driverを入れておく
Personal > ${デプロイしたアプリ} > Settings の Add buildpack
に
https://github.com/heroku/heroku-buildpack-chromedriver.git
https://github.com/heroku/heroku-buildpack-google-chrome.git
を追加すると
Your new buildpack configuration will be used when this app is next deployed.
なんて出てくる。
要約すると**次のデプロイ時に反映するよ!**ってことなのかな。
出来上がり
最終的にデプロイしたものがこちら
ソース
require 'net/http'
require 'uri'
require "selenium-webdriver"
class Line
TOKEN = ENV["TOKEN"]
URI = URI.parse("https://notify-api.line.me/api/notify")
def make_request(msg)
request = Net::HTTP::Post.new(URI)
request["Authorization"] = "Bearer #{TOKEN}"
request.set_form_data(message: msg)
request
end
def send(msg)
request = make_request(msg)
response = Net::HTTP.start(URI.hostname, URI.port, use_ssl: URI.scheme == "https") do |https|
https.request(request)
end
end
end
caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {binary: "/app/.apt/usr/bin/google-chrome", args: ["--headless"]})
driver = Selenium::WebDriver.for :chrome, desired_capabilities: caps
wait = Selenium::WebDriver::Wait.new(timeout: 10)
i = 0
begin
driver.get "https://tvguide.myjcom.jp/search/event/?keyword=[%E7%94%9F]%20%E3%83%A4%E3%82%AF%E3%83%AB%E3%83%88&keywordTarget=title&channelType=120&channel=147_65406%2C219_65406%2C146_65406%2C166_65406%2C168_65406%2C169_65406%2C158_65406%2C132_65534%2C176_65406%2C138_65406"
i += 1
wait.until { driver.find_element(:css, 'div.program_list:nth-child(2) div.detail').displayed? }
rescue => e
if i <= 5
retry
else
driver.quit
raise
end
end
detail = driver.find_element(:css, 'div.program_list:nth-child(2) div.detail')
ttl = detail.find_element(:css, 'p.ttl').text
date = detail.find_element(:css, 'p.date').text
chttl = detail.find_element(:css, 'p.chttl').text
msg = "\n" # lineに送信する内容
# date に当日の日付が含まれているかどうか
if date.include?(Time.new.strftime("%-m月%-d日"))
msg << "今日は\n#{date}\n#{ttl}\n#{chttl}\nで見られます"
else
msg << "試合なし"
end
driver.quit
line = Line.new
line.send(msg)
なんか全くrubyっぽくない気がしないでもないけど、とりあえず支障なく動けば見てくれなど気にしない…
- 結局
retry
は5回、試合がない日は"試合なし"
と表示されるようにした。 - Heroku Scheduler は
Daily at 3:00 AM UTC
に設定(UTCだとこれで12時)
動作確認
$ heroku run "ruby jcom_scrape.rb"
今日は
4月24日(水) 17:50~23:00 (310分)
[生]SWALLOWS BASEBALL L!VE 2019 東京ヤクルト×巨人
Ch.753:フジテレビONE スポーツ・バラエティ
で見られます
これが毎日12時にLINEで送られてきます。
Nokogiri使ってた頃とほぼ変わらない情報が得られました。
感想
普段文章なんて書かないので、記事を書くことがこんなにも大変なのかと思いました(KONAMI)
なんか作りたいけどアイデアがないのでなかなかできませんが、作りたいものがあったらまた書いてみようかな。
アウトプットって難しいね。