さて、Rubyを使ってWebスクレイピングをするときは、個人的にはNokogiriをよく使っています。
通常はNet::HTTPでとってきたHTMLをそのままNokogiriにぶち込めばOKなのですが、非同期での描画を行うAjaxのサイトの解析はそのままではできません。
そこで、PhantomJSのRubyフロントエンドであるPoltergeistをドライバとしてCapybaraを組み合わせると、深く考えることなく静的ページと同じ感覚で解析ができてしまいます。要は、見えないところでまるっとブラウザを動かしてしまって、DOMから仮想的なHTMLを生成させちゃってNokogiriにぶち込む…というようなアプローチです。
require 'nokogiri'
require 'capybara'
require 'capybara/poltergeist'
def wait_for_ajax(session)
# https://robots.thoughtbot.com/automatically-wait-for-ajax-with-capybara
Timeout.timeout(Capybara.default_wait_time) do
return if session.evaluate_script('jQuery.active').blank?
loop until session.evaluate_script('jQuery.active').zero?
end
end
def access(url)
Capybara.register_driver(:poltergeist) do |app|
Capybara::Poltergeist::Driver.new(app, {
js_errors: false #JSに問題があったとき例外を吐かせない。スクレイピングの際は常にfalseがいいです。
})
end
s = Capybara::Session.new(:poltergeist)
s.visit(url)
wait_for_ajax(s)
s
end
def get_html(url)
s = access(url)
html = s.html
s.reset!
s.driver.quit
html
end
#あとは普通にNokogiriにぶっこんでゴリゴリやってくだけ!
page = Nokogiri::HTML.parse(get_html(url))
page.css('body')
ちなみに、Capybara::Sessionのインスタンス(accessメソッドの戻り値)に対して、ちょうどfeature specで書くようなマッチャを使ってページに対する操作ができるので、例えば「ここをクリックすると出てくる情報を解析したい」という作業も簡単にできます。
s = access(url)
s.click('Submit')
s.find('.text') #findを呼ぶとマッチする要素が現れるまで再描画を(デフォルトで2秒間)待ってくれます。
s.html #これをNokogiriにぶち込めばOK
後ろでブラウザがまるごと動いていることの弊害と言ってはあれですが、スクレイピングにあたっては必要ではない情報も非同期でゴリゴリ取ってくることになり、負荷や時間などが増大します。
ということで、そういった必要でない情報は取ってこないようにして少しでも時間と相手サーバの負荷を低減する方法について。
Blacklisting
普通にNet::HTTPでページを単体で取ってくるのと異なり、PhantomJSの中でWebKitさん(QtWebKitさん)が完全にページを描画していて、画像からCSSからJSから、同期非同期にかかわらず全ての情報をとってくるため、時間や計算負荷は馬鹿になりません。
特に、Google AdWordsやFacebook Widgetなど、スクレイピングするにあたっては活用できない情報なども取りに行くので、これらは取ってこないようにしたいです。
そのために、PoltergeistはURLに基づくBlacklistを指定できるようになっていました。
これで、描画にあたって指定したURLにマッチするリクエストは送られず、描画完了までの時間が大幅に改善します。
... s = Capybara::Session.new(:poltergeist) s.driver.browser.url_blacklist = %w[www.facebook.com] s.visit(url) ...
Whitelisting
さらに、2015年末、ホワイトリスト方式でURLを制限できる機能がPoltergeistに取り込まれました。
https://github.com/teampoltergeist/poltergeist/pull/701
「どのURLを弾くか」を(実際のアクセスを見ながら)一つ一つ見つけては指定していくよりも、「狙ったサイト以外へのリクエストを全部弾く」とできるので、スクレイピングではとても助かります。
...
s = Capybara::Session.new(:poltergeist)
s.driver.browser.url_whitelist = %w[www.jleague.jp]
s.visit(url)
...
[ruby]
もちろんホワイトリストだけで対応できない場合もあります。
必要なコンテンツやJSフレームワークがサイト外に置かれていて、それらをいちいちホワイトリストに追加するより弾きたいアクセスをブラックリストに追加しておくほうがラク…とかあります。
場合によりけり。
<h5 id="hs_287a558c726964c445967a369e6da2db_header_2"> 画像をロードしない</h5>
ついでに、PhantomJSのオプションとして、画像を実際には読み込まないという指定が可能で、これも時間の改善に役立ちます。
[ruby]
Capybara.register_driver(:poltergeist) do |app|
Capybara::Poltergeist::Driver.new(app, {
js_errors: false,
phantomjs_options: ["--load-images=no"]
})
end
ベンチマーク
AjaxゴリゴリのサイトであるJリーグ公式サイトhttp://www.jleague.jp/match/j1/2016/022801/live/の描画完了までの時間を10回平均で求めてみました。
このサイトは非同期描画の際のコンテンツは全てwww.jleague.jpからとってきており、それと別にfacebookやgoogleから情報をとってきていますので、それぞれをホワイトリスト・ブラックリストとして各組み合わせで計測しました。
| 画像無視 | Blacklisting | Whitelisting | 平均描画時間 |
|---|---|---|---|
| – | – | – | 5.46秒 |
| – | ○ | – | 4.54秒 |
| – | – | ○ | 3.51秒 |
| ○ | – | – | 2.67秒 |
| ○ | ○ | – | 2.55秒 |
| ○ | – | ○ | 1.18秒 |
今回は対象サイトがサイト内からのみ情報を取ってきているという設計であったため、やはりホワイトリストによって無駄なリクエストを完全に排除してしまうのが、Ajax対応スクレイピングをするにあたっての描画時間短縮に関してはもっとも効果的です。
状況によりけりですが、スクレイピングの際にはそういったケースがおそらく主になってきそう。
ベンチマークのコードはこんなかんじ。
ホワイトリストとブラックリストの同時指定は原理的には意味が無いので、出さないようにしています。
サイト外へのアクセスや画像だけでなく、CSSのリクエストなども無視できるとさらに良いのですが、Poltergeist経由では今はできないようです(PhantomJSを直に叩く時はnodeでなんかできるらしいです)。
Capybara+Poltergeistをかませてのスクレイピングは便利ですが、非同期描画を行っていないサイトに対してこれを行うのは余計な通信や処理を発生させるだけで意味がないので、サイトに応じて必要なときだけこのやり方で、基本的にはシンプルにNet::HTTPでHTML単品だけ取ってきてNokogiriで刻むのが良いです。
なお当然の話ですが、スクレイピングする上での義務として、先方のサーバ負荷やコンテンツの権利などに対して注意を払いましょう。