動的に読み込まれるテレビ番組表をスクレイピング【Python】【Selenium】

#概要
[Yahoo!テレビ.Gガイド](https://tv.yahoo.co.jp/listings/42/?&st=24&s=1&va=24&vb=1&vc=0&vd=0&ve=1&d=20200421)からテレビ番組情報をスクレイピングするコードを書きました。
工夫したポイントは２点です。

* 番組情報はページ表示後に動的に読み込まれるため、Pythonの`requests`などだとうまく取得できません。
そこで`selenium`を使うことでこの問題を解決しました。
* 開発はMacとラズパイでしていたため、両環境でコードを共有できるように、OSに応じて一部処理を分岐させるようにしました。

今回はとりあえず開始時刻、チャンネル、番組タイトルを下記のようにcsvとして出力してみました

```
開始時刻,チャンネル,番組タイトル
21:54,5,報道ステーション
22:30,1,歴史秘話ヒストリア「弘前城　北のお城の400年」
22:50,2,ネコメンタリー　猫も、杓子（しゃくし）も。「吉田修一と金ちゃん銀ちゃん」
23:00,4,news zero新着「軽症」自宅待機の男性(52)死亡…ベッドの空き待ちで
23:00,6,NEWS23　小川彩佳　▽上白石萌音テレワークであの名曲をカバー
23:00,7,WBS▽スーパーが大混雑…3密どう防ぐ?買い物代行とは▽アイリスがマスク増産決定
23:00,8,TOKIOカケル【江口洋介&滝藤賢一が意外な夫婦生活を告白!名作ドラマも公開】
...
```
#注意
スクレイピングは使い方によっては違法とみなされる場合があります。
[利用規約](https://support.yahoo-net.jp/PccTv/s/)や[robots.txt](https://tv.yahoo.co.jp/robots.txt)も都度参照し、本記事に含まれるコードの利用については各自の判断・自己責任でお願いします。
#環境
macOS Catalina バージョン10.15.4 かつ python3.8
または
Raspberry Pi 3 Model B+ かつ Rasbian Stretch かつ python3.5
#準備
##Macとラズパイ共通
beautifulsoup、seleniumをpipでインストールします

```
pip install beautifulsoup4
pip install selenium
```
##Macのみ
ChromeブラウザがPCにインストールされていなければインストールします。
https://www.google.com/intl/ja_jp/chrome/

pipでchromedriverをインストールします。
Chromeブラウザのバージョンとあったものをインストールしてください。

```
pip install chromedriver-binary==<Chromeバージョン番号>
```
Chromeのバージョン確認、バージョン指定によるchromedriverインストールの参考ページ：
[selenium向け ChromeDriverをpipでインストールする方法（パス通し不要、バージョン指定可能）]
(https://qiita.com/hanzawak/items/2ab4d2a333d6be6ac760)
##ラズパイのみ
chromiumのdriverをインストールします。
（今回、たまたま下記で動きましたが、正直そこまでよくわかっていません。ブラウザとバージョンが異なるなどで動かないような場合もあると想定されますが、そのような場合にどう対処すればよいかは未確認です）

```
sudo apt-get install chromium-chromedriver
```
参考：Raspberry PiにSeleniumとchromedriverでブラウザ操作
[https://www.miki-ie.com/raspberry-pi/raspberry-pi%E3%81%ABselenium%E3%81%A8chromedriver%E3%81%A7%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E6%93%8D%E4%BD%9C/](https://www.miki-ie.com/raspberry-pi/raspberry-pi%E3%81%ABselenium%E3%81%A8chromedriver%E3%81%A7%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E6%93%8D%E4%BD%9C/)
#コード
2020年4月21日現在動作することを確認していますが、今後ページの構造が変われば機能しなくなる可能性がありますのでご留意ください。
以下では番組タイトル、開始時刻、チャンネル情報を取得しています。
具体的にhtmlから情報を取得する部分は、用途に合わせて適宜書き換えてください。


```Python
from bs4 import BeautifulSoup
from selenium import webdriver   
import platform
#Macとラズパイどちらでも同じコードで動くように、OSによって処理を分岐する
OS = platform.system()
if OS == 'Darwin': #Macの場合
    import chromedriver_binary
elif OS == 'Linux': #ラズベリーパイの場合
    pass

output_file_path = 'program.csv'

area = '23' #どの都道府県の番組表を表示するか。23は東京都。
date = '20200421' #何年何月何日の時刻表を表示するか。
starttime = '20' #何時以降の番組表を表示するか。
duration_hour = '6' #何時間分の番組表を表示するか。

url = 'https://tv.yahoo.co.jp/listings/?'
#地域情報を追加。省略（コメントアウト）可。省略時のデフォルト値は23(東京都）
url += ('a='+area+'&')
#日付情報を追加。省略（コメントアウト）可。省略時のデフォルト値は現在日
url += ('d='+date+'&')
#時刻情報を追加。省略（コメントアウト）可。省略時のデフォルト値は現在時
url += ('st='+starttime+'&')
#表示対象時間情報を追加。省略（コメントアウト）可。省略時のデフォルト値は6（単位は時間）
url += ('va='+duration_hour+'&')

#webdriverの取得。このあたりでエラーが出る場合はchromedriver-binaryのバージョン違いを疑う
options = webdriver.ChromeOptions() 
options.add_argument('--headless') 
driver = webdriver.Chrome(options=options)

#Webページを読み込み、htmlを取得して、beautifulSoupでパース
driver.get(url)
html = driver.page_source.encode('utf-8')
soup = BeautifulSoup(html,'html.parser')

#番組表情報の取得

#番組表の上部に書かれているチャンネルリストの取得
station_elems = soup.find_all('td', class_='station')
stations = [elem.text.split('ch')[0] for elem in station_elems]
#番組タイトルを含む要素の取得
title_elems = soup.find_all('a', class_='title')

table = [['開始時刻','チャンネル','番組タイトル']]
for elem in title_elems:
    #タイトルの取得
    title = elem.text
    #開始時刻の取得
    starttime = elem.parent.find('span',class_='time').text
    #番組表のどの列にかかれている情報か
    col = int(elem.get('data-ylk').split('pos:')[1])
    #列番号からチャンネル番号を取得
    channel = stations[col-1]
    #番組タイトル、チャンネル、開始時刻を要素に追加
    table.append([starttime,channel,title])
    
#csv形式で保存
with open(output_file_path,'w') as f:
    f.write('\n'.join([','.join(v) for v in table])) 
```

#参考
How to spend the terminal - テレビ番組表をスクレイピング
[http://moxtsuan.hatenablog.com/entry/scrape-tvprogram](http://moxtsuan.hatenablog.com/entry/scrape-tvprogram)