お久しぶりです、Sakaeです。

クローリングするにも様々なツールがありますが、その中でもWebブラウザを直接操作することが可能なSeleniumは特別な存在感を放ちます。

ヘッドレスブラウザを利用したクローリングでは昔はPhantomJS X capybaraという組み合わせもありましたが、PhantomJS亡き今はSeleniumにヘッドレスモードのChromeを組み合わせるのがモダンです。

ルーターブログでもこれまで度々SeleniumとChromeについて取り上げてきました。

ブラウザを起動するためメモリ利用率を圧迫するのが玉にキズですが、その半面、ブラウザと同じ挙動でDOMを展開し、JavaScriptなどもガンガンに実行し、HTTPリクエストを送るのではなくボタンを押すという人間めいたアクションができ、こちらが動かしたいJavaScriptも実行できてしまうという、オールインワンのクローラー離れした良い点を多数備えるSelenium。

クローリングエンジニアとして、このSeleniumの一番おもしろいところは、直接ブラウザ上でクローラーがフォーム入力やクリックなどを行って、ページ遷移していく様を目視で確認できるところです。

今回、このブラウザを目で見ながらクローラー構築する方法を便利にする簡易ツールを作成しましたので、そのツールを使いながらSeleniumでのクローラー構築方法をご紹介していこうと思います。

Selenium便利デバッガー"pranium"について

pryのようなSeleniumデバッガーが欲しい

Rubyの開発者におなじみのpryという対話式のコンソールデバッグツールをご存知でしょうか。一行づつRubyコードを入力し、実行結果がすぐに返答される。この対話を通して一つ一つソースコードの動作を確認していきます。

Seleniumでクローラーを構築する際、Chromeを自動操縦するソースコードを書き換えてはChromeの動作を見守り、間違いを発見。また書き換えてはChromeの動作を見守り、…(ry

上記のようなソースコード修正・実行のループを防ぎpryのようにリアルタイムにコードを実行して、期待の動作でなかった場合はその場でエラーを受け取り、その場で修正したコードを再実行するという効率的なデバッグループを、Selenium+Chrome環境のコーディングでも回したいというのが願いです。

pryのようなSeleniumデバッガ、praniumを作りました

25分くらいで作成した、全く公開できるような完成度ではないしろものですが、もうgithubに公開しました。正直ただのクローラーにうぶ毛が生えたレベルです。classすら作成していないただの直列コードです。しかも今の所MacOS限定です((((;゚Д゚)))) 今後少しづつ直していきます。

meguroman/pranium: This is selenium debugging tool like a pry debug

動作概要

詳細な使い方は後述しますが、下記のgif動画のように一旦praniumのプロセスを起動したら、あとはSeleniumを自動操縦するためのRubyコードを1行づつ書いていくだけです。コードの入力と同時にChrome上に実行結果が反映されます。

pranium_how_to_use

praniumを使ったSelniumでのクローラー構築

使い方基礎編

まずはpraniumの起動と終了、簡単な実行方法です。

praniumを起動するとwaiting ... please enter your selenium script.との表示があり、コードの入力を求められます。

例えば pp 'Hello Pranium!'と入力すると、以下のように[Execution result]という欄に"Hello Pranium!"というppの出力結果が返ってきます。

----------の線で囲まれた範囲が、一回のコード入力とそれに対するRuby(Selenium)のレスポンスになります。一行コードを書いて出力結果の確認を延々と繰り返すのがpraniumのお仕事です。

$ bundle exec ruby pranium.rb
waiting ... please enter your selenium script. exp) driver.navigate.to "https://example.com"
selenium object is "driver". if you want to exit, please enter just "exit".
-----------------------------------
waiting ... please enter your selenium script.
pp 'Hello Pranium!'  <ーここに任意のコードを入力

[You Enterd]
pp 'Hello Pranium!' <ー入力されたコード

[Execution result]
"Hello Pranium!"   <ーコードの実行結果

また、入力を間違えてpputsのように存在しないメソッドを呼んで、Rubyの構文エラーや実行時エラーで怒られてしまった場合は、[Execution result]にRubyのエラーが返ってきます。

・・・略・・・
-----------------------------------
waiting ... please enter your selenium script.
pputs

[You Enterd]
pputs

[Execution result]
=== Ruby ERROR OCCURRED ===
"undefined local variable or method `pputs' for main:Object\n" +
"Did you mean?  puts"
["pranium.rb:14:in `eval'",
 "pranium.rb:14:in `eval'",
 "pranium.rb:14:in `wait_key_input'",
 "pranium.rb:26:in `block in <main>'",
 "pranium.rb:24:in `loop'",
 "pranium.rb:24:in `<main>'"]
-----------------------------------
waiting ... please enter your selenium script.

praniumを終了したい場合はexitを入力してください。

・・・略・・・
-----------------------------------
waiting ... please enter your selenium script.
exit

[You Enterd]
exit

[Execution result]

以上が基本的なpraniumの使い方です。

Seleniumで検索サイトで検索結果を取得するクローラーをpraniumでデバッグしながら作る

それでは、実際にpraniumを使ってクローラーを作っていきます。

Chrome自動操縦で目的ページへ遷移する

まずはpraniumを起動してください。

$ bundle exec ruby pranium.rb
waiting ... please enter your selenium script. exp) driver.navigate.to "https://example.com"
selenium object is "driver". if you want to exit, please enter just "exit".

-----------------------------------
waiting ... please enter your selenium script.

しばらくすると下図のような何も表示されていないChromeのWindowが、ひっそりとターミナルの裏側で起動しているので、chromeの存在を確認してあげて下さい。

chrome_blank_window

次に、seleniumによるChrome自動操縦のHello Worldにあたるdriver.navigate.toメソッドを使ってYahoo.com(米国Yahoo)に遷移してみましょう。

ちなみに、pranium起動時のメッセージにも表示されますが、seleniumのWindowオブジェクトはdriverという名前になります。それ以外は受け付けません。異論は受け付けます。

・・・略・・・
-----------------------------------
waiting ... please enter your selenium script.
driver.navigate.to "https://www.yahoo.com/"

[You Enterd]
driver.navigate.to "https://www.yahoo.com/"

[Execution result]

praniumにdriver.navigate.to "https://www.yahoo.com/"を入力すると、自動操縦中ChromeにYahoo.comのトップページが表示されます。praniumと裏側のChromeがつながっている一体感を感じて下さい。

chrome_yahoo.com_top

検索窓(フォーム)にテキストを入力する

次に、Yahoo.comの検索窓に検索キーワードを入力したいところですが、どのように「検索窓に入力する」という動作を実現すればよいかを考えます。

まずは無意識レベルでOption + ⌘ + iを入力し、Chrome Developer Toolsを起動し、検索窓を表すCSSセレクタを探し出して下さい。この場合は下図の通り♯uh-search-boxがそれに当たりますね。

chrome_yahoo.com_enter_searchbar01

そしていよいよ検索窓への入力を実現しますが、ここでポイント。Seleniumのフォーム入力といえばsend_keysメソッドかと思いますが結構クセがあって、ページによって失敗したりすることがあります。そして過去を振り返るとHeadless Chromeが登場したばかりのころはchromedriverに問題があり、send_keysが使えないという時期がありました。

そんな思いを経て、僕的なベストプラクティスですが、ページ内の操作はもうJavaScriptでやってしまえという考えに至りました。

このJavaScriptでやってしまうことの最大のメリットはChrome上で動きを見ながらデバッグできることです。

下図のようにChrome Developer toolsのConsole画面から、♯uh-search-boxというCSSセレクタに対してJavaScriptコードを書きなぐりながら、その動作を目でみて確認していきます。まずはこのConsole上で動作することを確認して下さい。

chrome_yahoo.com_enter_searchbar02

Console上で正しい動作を確認したら、後はSeleniumにそれを実行させるだけです。seleniumでJavaScriptを実行するにはdriver.execute_script()メソッドを使います。このメソッドの引数に先程のConsole上でデバッグ済みの検索窓にテキストを入力するJavaScriptコードを渡します。

具体的にはdriver.execute_script("document.querySelectorAll('#uh-search-box')[0].value = 'Naomi Osaka, Great!'")のようなコードになりますのでこれをpraniumで実行します。

なお、上図のとおりChrome Developer toolsのConsoleデバッグによってすでに自動操縦中ChromeのYahoo.comの検索窓にテキストが入っていますので、これはChrome上で削除しておきます。(普通の手動操作で、マウスで検索窓をクリックしてバックスペースキーで削除してOKです。)

・・・略・・・
-----------------------------------
waiting ... please enter your selenium script.
driver.execute_script("document.querySelectorAll('#uh-search-box')[0].value = 'Naomi Osaka, Great!'")

[You Enterd]
driver.execute_script("document.querySelectorAll('#uh-search-box')[0].value = 'Naomi Osaka, Great!'")

[Execution result]

JavaScriptコードをpraniumで実行すると、下図のようにChromeの検索窓にテキストが入力されているはずです。

chrome_yahoo.com_enter_searchbar03

検索窓(フォーム)の検索ボタンを押してフォーム送信する

残すは「検索窓の横にある検索ボタンを押す」という動作です。ここまで実行できれば念願の検索結果を見ることができます。嬉しいですね。

手順は先程と同じです。自信があればいきなりpraniumにseleniumの実行ソースを入力していただいて問題ありませんが、一旦Chrome Developer toolsのConsoleウィンドウでJSレベルのデバッグを行うことを推奨します。

ということで、まずは検索ボタンのCSSセレクタを探します。下図の通り♯uh-search-buttonというCSSセレクタが該当しそうです。

chrome_yahoo.com_push_searchbar01

前述の検索窓へのテキスト入力と同じように、特定したCSSセレクタに対して検索ボタンをクリックするという動作をConsoleタブを使って検証、初期デバッグしていきます。

JavaScriptを使ってクリック動作を行うにはdocument.querySelectorAll('#uh-search-button')[0].click()というJSコードを用います。これをConsole上で実行すると、

chrome_yahoo.com_push_searchbar02

このように夢にまで見た検索結果一覧画面が表示されました。

chrome_yahoo.com_push_searchbar03

これでJSコードレベルでのデバッグは済んでいますので、あとはSeleniumでそれを実行できるか試すだけです。

現在すでに自動操縦中Chromeには検索結果一覧画面が表示されていますが、これからSeleniumでのページ遷移テストのため、一旦、検索結果一覧ページからTopページに戻る必要があります。自動操縦中Chromeウィンドウの戻るボタンを手動で押して、Topページに戻って置きましょう。

その後、以下のようにpranium上で先程のJSコードをdriver.execute_scriptメソッドを用いて実行させます。

・・・略・・・
-----------------------------------
waiting ... please enter your selenium script.
driver.execute_script("document.querySelectorAll('#uh-search-button')[0].click()")

[You Enterd]
driver.execute_script("document.querySelectorAll('#uh-search-button')[0].click()")

[Execution result]

すると・・・

chrome_yahoo.com_push_searchbar04

Seleniumによる自動操縦でも正常にYahoo.comの検索結果一覧画面が表示されることが確認できます。これでSeleniumで任意のキーワードを使って検索結果一覧画面のDOMを取得するという一連の流れの検証が完了したことになります。

あとは、先程1行づつpraniumで試してきたソースコードを一つのRubyコードにまとめれば、クローラーが完成です。

本来はクローラー本体のRubyソースをいじってはChromeの動作を見守り、またソースを修正しては…(ry、を繰り返すという大きな手間が発生しますので、上記の流れでChromeの1操作ごとに確実に実行できることを確認していくことで非常に効率的なクローラー開発を進めることが可能です。

Seleniumの保持している情報(Cookieなど)を可視化する

pryのデバッグ機能として非常に便利なのが、今現在プログラムが保有している変数の中身など、プロセスの保持している情報を見ることができることです。

たとえばクローラー開発において、どのページに遷移したときに、どのようなCookieを保持しているのかを把握しておくことは非常に重要になります。Cookieの状態によっては、同じURLにアクセスしても、描画される画面が異なる、別ページにリダイレクトされるなど、Webサイト、WebアプリにとってCookieとWebページの遷移は直結しているからです。

praniumもクローラー(Chrome)が保持しているリアルタイムの情報を確認・変更することが可能です。例えば、Cookieを見たい場合はpraniumで以下のようにpp driver.manage.all_cookiesを実行してあげると、

・・・略・・・
-----------------------------------
waiting ... please enter your selenium script.
pp driver.manage.all_cookies

[You Enterd]
pp driver.manage.all_cookies

[Execution result]
[{:name=>"ymuid",
  :value=>"v=0DD361FACBE964F732CE6D8CCA956551&ts=1537921604",
  :path=>"/",
  :domain=>".search.yahoo.com",
  :expires=>
   #<DateTime: 2019-09-26T00:26:44+00:00 ((2458753j,1604s,335434933n),+0s,2299161j)>,
  :secure=>false},
・・・略・・・
 {:name=>"B",
  :value=>"8aldd81dqlkgq&b=3&s=1j",
  :path=>"/",
  :domain=>".yahoo.com",
  :expires=>
   #<DateTime: 2019-09-26T00:26:47+00:00 ((2458753j,1607s,41n),+0s,2299161j)>,
  :secure=>false}]

このように、ChromeのCookie情報を閲覧可能です。また、クローラーのテストのためにCookieを一つ追加してみましょう。added_cookieという名前のCookieをdriver.manage.add_cookie({name: 'added_cookie'})のように追加します。

・・・略・・・
-----------------------------------
waiting ... please enter your selenium script.
driver.manage.add_cookie({name: 'added_cookie'})

[You Enterd]
driver.manage.add_cookie({name: 'added_cookie'})

[Execution result]
=== Ruby ERROR OCCURRED ===
"value is required"
["pranium.rb:14:in `eval'",
 "(eval):1:in `wait_key_input'",
 "pranium.rb:14:in `eval'",
 "pranium.rb:14:in `wait_key_input'",
 "pranium.rb:26:in `block in <main>'",
 "pranium.rb:24:in `loop'",
 "pranium.rb:24:in `<main>'"]

あ、追加の方法が間違っていましたね。"value is required"のようにRubyからのエラーが返ってきました。Cookieを足す場合にはnameとvalueをセットで渡すのが必須なのです。

このように、間違ったコードを書くとその場で間違いを指摘してくれるのがpraniumを使ったデバッグ・検証の非常に嬉しいところです。

改めて、driver.manage.add_cookie({name: 'added_cookie', value: 'test!'})と修正してクローラーへのCookie追加をしてみます。

・・・略・・・
-----------------------------------
waiting ... please enter your selenium script.
driver.manage.add_cookie({name: 'added_cookie', value: 'test!'})

[You Enterd]
driver.manage.add_cookie({name: 'added_cookie', value: 'test!'})

[Execution result]

こんどはエラー無くCookie追加のRubyコードが実行できました。では、ただしくクローラーの保有Cookieが変更されているか再度Cookie一覧を確認してみると。

・・・略・・・
-----------------------------------
waiting ... please enter your selenium script.
pp driver.manage.all_cookies

[You Enterd]
pp driver.manage.all_cookies

[Execution result]
[{:name=>"added_cookie",
  :value=>"test!",
  :path=>"/",
  :domain=>"search.yahoo.com",
  :expires=>
   #<DateTime: 2038-09-21T00:27:46+00:00 ((2465688j,1666s,3n),+0s,2299161j)>,
  :secure=>true},
 {:name=>"ymuid",
  :value=>"v=0DD361FACBE964F732CE6D8CCA956551&ts=1537921604",
  :path=>"/",
  :domain=>".search.yahoo.com",
  :expires=>
   #<DateTime: 2019-09-26T00:26:44+00:00 ((2458753j,1604s,335434933n),+0s,2299161j)>,
  :secure=>false},
・・・略・・・
 {:name=>"B",
  :value=>"8aldd81dqlkgq&b=3&s=1j",
  :path=>"/",
  :domain=>".yahoo.com",
  :expires=>
   #<DateTime: 2019-09-26T00:26:47+00:00 ((2458753j,1607s,41n),+0s,2299161j)>,
  :secure=>false}]
-----------------------------------

added_cookieというnameのCookieが正常に追加されていました!

このように、今クローラーが見ているWebページを可視化して見れる。今クローラープロセスが保有している内部情報も可視化して見れる。これがpraniumを使った開発の便利なところです。

まとめ

Seleniumやヘッドレスブラウザを使う/使わないによらず、「我々が普通にChromeブラウザで見ている画面と、クローラーが見ている画面は違うかもしれない」という意識を持っておくことは、クローリングエンジニアとしては必要な心がけです。

そういう意味で考えると、ステップバイステップで人間とクローラーの齟齬を確認しながら開発ができるSelenium x Chromeを使ったクローラー開発は非常に便利ではあります。

が、冒頭でもお伝えしたとおりブラウザを実行してのクローリングは、リソースを食い、スピードが遅く、不安定、などなどデメリットもあるのが現実です。

ルーターのデータアグリゲーションサービスでは、クローリング対象サイトの特徴を見極め、ビジネス上の要件を理解したうえで、最適なクローラーの開発手法をご提案しています。

また、本記事でご紹介したように、より高度で効率的なクローリング手法の開発に関しても常に模索していますので、Web上/ネイティブアプリ上のデータ収集のご要件でお困りでしたら何なりとお問い合わせ下さいませ!