J:COMテレビ番組表のリニューアルでNokogiriが使えなくなったのでseleniumに移行した

# この記事は

J:COMのテレビ番組表を[selenium-webdriver](https://www.seleniumhq.org/)でスクレイピングして、当日のヤクルト戦の番組内容を[LineNotify](https://notify-bot.line.me/ja/)で知らせるスクリプトを[heroku](https://jp.heroku.com/)にデプロイ、1日1回定期実行させるまでのお話。
ただの趣味グラマの書きなぐり＆二番煎じどころじゃないので参考にならないと思います。

## 経緯

やきう（ヤクルト戦）見よ
↓
J:COMのリモコン使いづら！
チャンネル調べるのメンドクサ…
誰か今日のチャンネル教えてくれんかなー
↓
ほー、**LineNotify**なんてもんがあるのか
勉強がてらなんか作ってみるか
↓
毎日12：00にJ:COM番組表から取得した放送日時とチャンネルがLineで送られてきた！
やったね！

## 問題発生

**4/23 12:00**
あれ…？Lineから通知来たけど本文入ってないやん:scream:
とりあえず[J:COMのHP](https://c.myjcom.jp/tvguide_renewal.html?sc_pid=my_tvtop_top_tvguide-renewal)見てみる。

リニューアルか…
しゃーない、作り直すか。
そんなかからんやろ…

## スクレイピングする
### ruby + Nokogiri

この組み合わせの技術記事はネット上に（Qiitaにも）大量にあるし、自分自身も色々参考にさせてもらった。

[Ruby + Nokogiriでスクレイピング](https://qiita.com/kumamonmaster/items/9bb2aadde56c956fdc9f)
[Ruby製の構文解析ツール、Nokogiriの使い方 with Xpath](https://blog.takuros.net/entry/2014/04/15/070434)
[スクレイピングのためのNokogiri利用メモ](http://otn.hatenablog.jp/entry/20090509/p1)

もともとの環境

```
windows 10 Home

$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x64-mingw32]

$ nokogiri -v
Nokogiri (1.8.5)
```

スクレイピング部分

```ruby:jcom_nokogiri.rb
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
```

とりあえず動作確認。これでいけるだろ（ﾊﾅﾎｼﾞｰ）

```powershell:結果
$ ruby jcom_nokogiri.rb
""
""
""
```

Oh…なぜだ…

デベロッパーツールで確認しても特におかしなところは見当たらないんだがー。

### 調べた

どうやら動的ページはNokogiriではダメらしい。記事があった。
[RubyでSeleniumを使ってスクレイピング](https://qiita.com/tomerun/items/9cb81d7a98150ff22f53)
[JavaScript等を用いた動的ページをスクレイピングするためのライブラリSeleniumのRubyでの使い方](https://qiita.com/justin999/items/55fbfd5d4f9fc8e5e77d)

ふむ、このseleniumとやらを使えばイケるのか。

### ruby + selenium-webdriver

導入なんかはリンク先の通り。[selenium-webdriver](https://www.seleniumhq.org/)と[ChromeDriver](http://chromedriver.chromium.org/downloads)を入れる。
ChromeDriverはpathを通しておく。

改良したらこんな感じになった。

```ruby:jcom_selenium.rb
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を使うのはオススメしない](https://qiita.com/katsuya_ds/items/725840f11f5c7b314daa)

使用感はNokogiriとさほど変わらないと思った。Nokogiriが使えるならこれも使えると思う。
ただ、Nokogiriに比べて動作がもっさり。この辺は割り切るしかない。

あとサイトへの接続がうまく行かなかったりすると要素が取得できずタイムアウトして終了するので、適宜`retry`などで再接続したら安定する。

## LineNotifyとHerokuで1日1回定期実行
### LineNotify

正直http周りはまっっっったくと言っていいほどわかりません。苦手意識があるのでしっかり学びたいところ。
なのでLineNotifyに関してはこちらの記事を~~パクリ~~参考にさせていただきました…
[[Ruby] Line NotifyでLineにメッセージ送信する手順メモ](https://qiita.com/meguroman/items/38bda8c539ede8f9a02a)

詳しく知りたいならこの辺をしっかり読めばいいんだと思います。
[LINE Notify API Document](https://notify-bot.line.me/doc/ja/)
日本語なので英語嫌いでも安心！

### Heroku

Herokuに関してもQiitaだけでも溢れるほど記事があります。私なんかは職業プログラマでもなんでもないので、必要知識は基本すぐパクります。無から有を生み出すレベルにないので。

参考にしたのはこちら
[Herokuで単純なrubyスクリプトを定期的に実行する](https://qiita.com/kasei-san/items/909f17c2c42b7e3a8489)

今回はこれだけでは足りないのでこちらを参考に
[HerokuにChromeとChrome driverを入れておく](http://ikapblg.blog.fc2.com/blog-entry-147.html)

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.`なんて出てくる。
要約すると**次のデプロイ時に反映するよ！**ってことなのかな。

## 出来上がり
最終的にデプロイしたものがこちら
### ソース
```ruby:jcom_scrape.rb
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時）

### 動作確認

```powershell
$ heroku run "ruby jcom_scrape.rb"

今日は
4月24日(水) 17:50～23:00 (310分)
[生]SWALLOWS BASEBALL L!VE 2019　東京ヤクルト×巨人
Ch.753：フジテレビＯＮＥ　スポーツ・バラエティ
で見られます
```

これが毎日12時にLINEで送られてきます。
Nokogiri使ってた頃とほぼ変わらない情報が得られました。

## 感想

普段文章なんて書かないので、記事を書くことがこんなにも大変なのかと思いました（KONAMI）

なんか作りたいけどアイデアがないのでなかなかできませんが、作りたいものがあったらまた書いてみようかな。
アウトプットって難しいね。