Browserless.ioを試してみる
経緯
Pipedream上でSeleniumを使ってスクレイピングしてきたデータをゴニョゴニョしてSlackに投げるとかを作ろうと思ったのだけど、Pipedreamは環境をいじれない、つまりChromeドライバーを追加することができない。
似たようなことを考える人はやっぱりいるようで、公式フォーラムに投稿があった。その代替案として紹介されていた Browserless.io を少し試してみたのだけど、どういうサービスなのかがわかりにくいと感じたのでまとめる。
クラウドベースの無料プランを使う想定で、結論から言ってしまうと、Browserless.io はどうやらヘッドレスブラウザ環境だけを提供してくれて実行環境は別に用意しないといけない様子。
公式のQuick Startを見るといきなりコードからスタートしてて、どこでコードを動かすんだろうと思うけど、一般的にこういうサービスは実行環境も提供してくれるもの、という思い込みがよくなかった。
管理画面はこんな感じ。
どこにもコードを記述するような場所は見当たらない。
ということで、今回はGoogle Colaboratoryを実行環境とする。
ちなみにGoogle Colaboratory「だけ」でSeleniumを動かす場合は、パッケージの追加、つまり環境構築が必要になる。
Browserlessを使うと、この環境構築が不要になり、PythonのSeleniumパッケージだけインストールで切れればよいということになる。
まず、Browserlessの管理画面でAPIキーを確認しておく。
でGoogle Colaboratoryでスクレイピングのコードを書く。参考までにJRAの今週末開催予定の各競馬場の馬場情報を取得してMarkdownで出力するコードを書いてみた。
!pip install selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.alert import Alert
import time
import textwrap
places = {}
url = "https://www.jra.go.jp/keiba/baba/index.html"
header = '''
### 目次
[:contents]
'''
chrome_options = webdriver.ChromeOptions()
chrome_options.set_capability('browserless:token', 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') # APIキーをセット
chrome_options.add_argument("--incognito")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument('--headless')
driver = webdriver.Remote(
command_executor='https://chrome.browserless.io/webdriver',
options=chrome_options
)
driver.get(url)
time.sleep(3)
place_element = driver.find_element(By.XPATH,'//*[@id="contentsBody"]/div[1]/div')
for a in place_element.find_elements(By.TAG_NAME,'a'):
places[a.text] = a.get_attribute("href")
print(header)
for k, v in places.items():
driver.get(v)
time.sleep(3)
# クッション値
cushion_time = driver.find_element(By.XPATH,'//*[@id="cushion_list"]/option[1]').text
cushion_value = driver.find_element(By.XPATH,'//*[@id="cushion_num"]/p/strong').text
# 含水率
moist_time = driver.find_element(By.XPATH,'//*[@id="moist_list"]/option[1]').text
turf_moist_value_goal = driver.find_element(By.XPATH,'//*[@id="turf_line"]/td[1]').text
turf_moist_value_4c = driver.find_element(By.XPATH,'//*[@id="turf_line"]/td[2]').text
dart_moist_value_goal = driver.find_element(By.XPATH,'//*[@id="dirt_line"]/td[1]').text
dart_moist_value_4c = driver.find_element(By.XPATH,'//*[@id="dirt_line"]/td[2]').text
# 芝コース
# - XPATHだと競馬場によってはコケる場合があるのでCSSセレクタで
turf_course = driver.find_element(By.CSS_SELECTOR, '#turf_info > div.grid > div.cell.data > div.course > ul > li > div > div.content').text
turf_status = driver.find_element(By.CSS_SELECTOR, '#turf_info > div.grid > div.cell.data > div.turf_condition > ul > li > div > div.content').text
# 週刊情報
week_info = driver.find_element(By.XPATH, '//*[@id="week_info"]/div[3]/div[1]').text
print(textwrap.dedent('''
### {p}
#### 馬場状態
##### 芝
- コース
- {tc}
- クッション値
- {cv}%
- 含水率
- {tmvg}
- 状態
- {ts}
##### ダート
- 含水率
- {dmvg}
#### レース回顧
-----
'''.format(p=k, cv=cushion_value, ct=cushion_time, tmvg=turf_moist_value_goal, dmvg=dart_moist_value_goal, mt=moist_time, tc=turf_course, ts=turf_status, wi=week_info)[1:-1]))
driver.close()
driver.quit()
実行結果は以下。
### 目次
[:contents]
### 福島競馬場
#### 馬場状態
##### 芝
- コース
- Aコース(内柵を最内に設置)
- クッション値
- 8.8%
- 含水率
- 11.5%
- 状態
- 第1回福島競馬終了後、競走により傷んだ3コーナーから4コーナーにかけての内柵沿いおよび発走地点等約2,500平方メートルの芝張替を実施しました。その後も施肥や散水等の管理作業を行なうことで芝の生育促進に努めました。芝の生育は順調で、全体的に概ね良好な状態です。
##### ダート
- 含水率
- 11.7%
#### レース回顧
-----
### 中京競馬場
#### 馬場状態
##### 芝
- コース
- Aコース(内柵を最内に設置)
- クッション値
- 8.7%
- 含水率
- 13.1%
- 状態
- 第2回中京競馬終了後、2コーナーの傷みが生じた箇所を中心に約800平方メートルの芝張替作業を実施しました。その他の箇所も蹄跡補修、洋芝の追加播種を行い、芝の生育促進に努めました。芝の生育は順調で、全体的に良好な状態です。
##### ダート
- 含水率
- 12.5%
#### レース回顧
-----
### 函館競馬場
#### 馬場状態
##### 芝
- コース
- Aコース(内柵を最内に設置)
- クッション値
- 7.4%
- 含水率
- 14.3%
- 状態
- 3コーナーから4コーナーの内柵沿いに傷みがありますが、その他の箇所は概ね良好な状態です。
##### ダート
- 含水率
- 9.1%
#### レース回顧
-----
ポイントはここ。
from selenium import webdriver
(snip)
chrome_options = webdriver.ChromeOptions()
chrome_options.set_capability('browserless:token', 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') # APIキーをセット
chrome_options.add_argument("--incognito")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument('--headless')
driver = webdriver.Remote(
command_executor='https://chrome.browserless.io/webdriver',
options=chrome_options
)
driver.get(url)
リモートにAPIキーでアクセス可能なChromeが存在してて、それに対して、いろいろなブラウザ操作を行うリクエストを送るという感じになっている。
料金プラン
基本的に個人で使うには有料プランはちょいお高めの印象。で、クラウドベースの無料プランについてざっくり。
- 同時最大1000ブラウザ
- 「ユニット」という単位で換算される。概ね1ブラウザセッション=1ユニットで考えれば良さそう。
- 1ユニットは最大30秒間使用される想定。これを超えるセッションは30秒ごとに課金される。無駄なセッション使用を防ぐためセッションは15分でタイムアウト(つまり、タイムアウト時は1セッションで30ユニット使用されると思われる)
- 無料プランの場合は1000ユニット/月
使用状況は管理画面で見れる。
先ほどのスクリプトだと1ユニット使用されていた。で、最初に別のスクリプトでいろいろ試してたときにどうやらタイムアウトしてたみたいで、ユニット数がぐっと使用されている模様。
ここから推測するに
- 定期的にシンプルなスクレイピングをする程度なら無料でも使えそう。
- エラー処理や終了処理はきちんと行っておかないと、意図せずタイムアウトして無駄にユニットを消費しそう。
という感じ。ちょっと計算内容には釈然とはしない感があるので、気をつけたほうが良さそう。
メリット
正直何が便利なの?という感はあるが、
- 実機では難しい大量アクセスをエミュレートするとかにはいいかもしれない。
- 最初に書いた通り、Seleniumのためのヘッドレスブラウザ環境を作れない場合、今回の例でいうとPipedreamのようなPaaSと組み合わせるにはいいかもしれない。
というところなのかな。かなりニッチなサービスという印象ではあるが、とりあえずPipedreamで定期的にスクレイピングを行いたい、かつ、requestやBeautifulSoupでは賄えない場合、の使い方としては納得した。
なんかタイムアウトしてた件、一応手元のコードでもdriver.quit()
はしてたんだけど、グラフ見る限りは同じ山になってるし、恐らくうまく終了できてなかったのでは、と推測。なんでかはわかんない。ただtry〜finalyみたいなことはしてなかったので、きちんと終了するようにしよう。