orangain flavor

じっくりコトコト煮込んだみかん2。知らないことを知りたい。

JavaScriptレンダリングサーバーSplashでスクレイピング

これはクローラー/Webスクレイピング Advent Calendar 2015の9日目の記事です。

本記事では、Scrapinghub社*1が開発しているSplashというオープンソースソフトウェアを紹介します。

github.com

JavaScriptを使ったページからスクレイピングする方法としては、PhantomJSとSelenium/CasperJSなどの組み合わせが一般的ですが、これらとは少し違う手段として使えるかもしれないソフトウェアです。 私自身Splashを最近知ったばかりで、軽く探した限りでは日本語の情報もないので、調査しつつSplashの使いドコロを探ってみたいと思います。

Splashとは

READMEには以下のように書かれています。

 Splash is a javascript rendering service with an HTTP API. It's a lightweight browser with an HTTP API, implemented in Python using Twisted and QT.

It's fast, lightweight and state-less which makes it easy to distribute.

(訳)SplashはHTTP APIを持つJavaScriptレンダリングサービスです。HTTP APIを持つ軽量ブラウザで、TwistedとQTを使ってPythonで実装されています。 高速、軽量かつステートレスで、容易に分散できます。

ドキュメントにはもう少し詳しく機能が書かれています。

  • process multiple webpages in parallel;
  • get HTML results and/or take screenshots;
  • turn OFF images or use Adblock Plus rules to make rendering faster;
  • execute custom JavaScript in page context;
  • write Lua browsing scripts;
  • develop Splash Lua scripts in Splash-Jupyter Notebooks.
  • get detailed rendering info in HAR format.

(訳)

要するに、以下のように動くということでしょう。なかなか面白そうです。

f:id:mi_kattun:20151209121519p:plain

Splashを使ってみる

とりあえず使ってみましょう。 今どきのソフトウェアらしく、Dockerを使って簡単にサーバーを起動できます。

$ docker pull scrapinghub/splash
$ docker run -p 5023:5023 -p 8050:8050 -p 8051:8051 scrapinghub/splash

ポートの意味はそれぞれ以下のとおりです。

以下のようにログが表示されたら起動完了です。この記事ではSplash 1.8を使います。

2015-12-08 13:20:59+0000 [-] Log opened.
2015-12-08 13:20:59.239310 [-] Splash version: 1.8
2015-12-08 13:20:59.240288 [-] Qt 4.8.1, PyQt 4.9.1, WebKit 534.34, sip 4.13.2, Twisted 15.4.0, Lua 5.2
2015-12-08 13:20:59.242303 [-] Python 2.7.3 (default, Jun 22 2015, 19:33:41) [GCC 4.6.3]
2015-12-08 13:20:59.242396 [-] Open files limit: 1048576
2015-12-08 13:20:59.242472 [-] Can't bump open files limit
2015-12-08 13:20:59.447814 [-] Xvfb is started: ['Xvfb', ':1069', '-screen', '0', '1024x768x24']
2015-12-08 13:20:59.507558 [-] proxy profiles support is enabled, proxy profiles path: /etc/splash/proxy-profiles
2015-12-08 13:20:59.547976 [-] verbosity=1
2015-12-08 13:20:59.548159 [-] slots=50
2015-12-08 13:20:59.548618 [-] Web UI: enabled, Lua: enabled (sandbox: enabled), Proxy Server: enabled
2015-12-08 13:20:59.549491 [-] Site starting on 8050
2015-12-08 13:20:59.549761 [-] Starting factory <twisted.web.server.Site instance at 0x1b87a70>
2015-12-08 13:20:59.551205 [-] SplashProxyServerFactory starting on 8051
2015-12-08 13:20:59.551413 [-] Starting factory <splash.proxy_server.SplashProxyServerFactory instance at 0x1b8a3b0>

起動したら、ブラウザでhttp://<Dockerのホスト>:8050/ にアクセスします。私の環境ではhttp://192.168.59.103:8050/でした。 このURLを開くと、以下のようなデモ用の画面が表示されます。 f:id:mi_kattun:20151209121440p:plain

「Render me!」という緑のボタンをクリックすると、以下のようにGoogleのページがレンダリングされた画像が表示されます。 f:id:mi_kattun:20151209121454p:plain さらに、スクロールするとネットワークに関する情報とHTMLが表示されます。 f:id:mi_kattun:20151209121508p:plain

「Render me!」ボタンを押すと、入力したURLにアクセスし、黒いテキストボックス内のLuaスクリプトが実行される、ということのようです。

SplashのAPIを使う

デモはこれぐらいにしてHTTP APIを使ってみましょう。 /render.html というエンドポイントにurlパラメータを渡すことで、指定したURLのHTMLを取得できます。

$ curl 'http://192.168.59.103:8050/render.html?url=http://google.com'
<!DOCTYPE html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head><meta content="世界中のあらゆる情報を検索するためのツールを提供しています。さまざまな検索機能を活用して、お探しの情報を見つけてください。" name="description"><meta content="noodp" name="robots"><meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
...

ここで重要なのは、このAPIで取得できるHTMLは、サーバーから取得したHTMLではなく、JavaScriptのonloadイベントが発生した時点でのDOMツリーをHTMLにしたものであるという点です。

このことがわかりやすい例として、PythonでさくっとWebスクレイピングする (JavaScript読み込みにも対応しつつ) - Qiita という記事でサンプルとして使われていた、テレ朝ニュースからスクレイピングしてみます。

news.tv-asahi.co.jp

このページでは、ページ下部の「関連ニュース」のリストがAjaxで読み込まれます。このため、以下のように普通にcurlで取得すると空の<div>要素が存在するだけです。

$ curl http://news.tv-asahi.co.jp/news_international/articles/000064029.html  | iconv -f shift_jis
...
<!-- 関連ニュース -->
            <div id="relatedNews"></div>
...

以下のようにSplashを経由させると、<div>要素の中にコンテンツが含まれた状態のHTMLを取得できます。onloadのタイミングまで待つので少しだけ時間がかかります。

$ curl -v 'http://192.168.59.103:8050/render.html?url=http://news.tv-asahi.co.jp/news_international/articles/000064029.html'
...
<!-- 関連ニュース -->
            <div id="relatedNews">                        <div class="kanrennews">
              <h3>関連ニュース</h3>

              <ul class="newslist clearfix">
                                  <li>
                  <div class="text"><a href="http://news.tv-asahi.co.jp/news_international/articles/000063920.html">「若い皆さん研究頑張って」 物理学賞・梶田さん</a><br><span>(2015/12/07 17:53)</span></div>
                </li>
                                <li>
                  <div class="text"><a href="http://news.tv-asahi.co.jp/news_international/articles/000063849.html">ノーベル賞大村さん現地に 梶田さん燕尾服を採寸</a><br><span>(2015/12/06 11:52)</span></div>
                </li>
                                <li>
                  <div class="text"><a href="http://news.tv-asahi.co.jp/news_international/articles/000063837.html">“ノーベルウィーク”大村さん、梶田さん現地到着</a><br><span>(2015/12/06 05:50)</span></div>
                </li>
                                <li>
                  <div class="text"><a href="http://news.tv-asahi.co.jp/news_international/articles/000063826.html">大村さんも現地到着 いよいよ「ノーベルウィーク」</a><br><span>(2015/12/05 17:31)</span></div>
                </li>
                                <li>
                  <div class="text"><a href="http://news.tv-asahi.co.jp/news_international/articles/000063810.html">ノーベル賞梶田さん到着 これからイベント目白押し</a><br><span>(2015/12/05 11:51)</span></div>
                </li>
                              </ul>

            </div>
            </div>
...

あとはお好きなスクレイピングライブラリでHTMLからスクレイピングすればOKです。

細かい話ですが、このページはShift_JISエンコードされているので、最初の例ではiconvを使って文字コードを変換しています。Splashを通すと、UTF-8エンコードされた状態で取得できるので、iconvは不要です。metaタグは<meta charset="Shift_JIS">のまま取得できるので、ちょっと紛らわしいですね。

render.htmlエンドポイントにはurl以外にも様々なパラメータを渡せます。例えばonloadからさらに指定した時間待つwaitや、ページ内でJavaScriptを実行するjs_sourceなどがあります。

また、他にも以下のようなエンドポイントがあります。

詳しくはAPIドキュメントを参照してください。

Splash HTTP API — Splash 1.8 documentation

Splashの使いドコロ

推測ですが、Splash開発のモチベーションは以下のようなものでしょう。

  1. JavaScriptを使ったページが増えており、JavaScriptへの対応は不可欠。
  2. しかしあらゆるページでPhantomJSを使うと遅くなってしまう。
  3. かといって、JavaScriptを使わないページはlxmlで、使うページはSelenium+PhantomJSなどと使い分けると、APIが違って面倒。
  4. Splashを使って、JavaScriptを実行したあとのHTMLをlxmlなどでスクレイピングすれば、JavaScriptに対応しつつAPIの一貫性を維持できる。

このため、lxmlなどを使って昔ながらのスクレイピングをしている人が、あまりコードを変更せずにJavaScriptに対応したいという目的で使えるのではないでしょうか。

一方でステートレス性を重視しているので、JavaScriptで対話的な操作をしてスクレイピングするという用途には向いていないように思います。サーバーでLuaスクリプトを実行できますが、それであれば正直SeleniumとかCasperJSとかでやっても同じように思います。

また、サーバーを立てる必要があるので、使い捨てのスクリプトにはあまり向かないでしょう。Scrapinghub社はクローラーSaaSを提供する会社であり、Splashもサービスの一つとして提供されています。このため、スケーラビリティが重視されており、ある程度大規模に使用しないと他の手段に比べてメリットを感じにくいかもしれません。

まとめ

SplashはJavaScriptレンダリングサーバーで、JavaScriptを使ったページからスクレイピングするための手段の一つとして使える可能性を持ったソフトウェアです。

ここで紹介したのは一番基本的な機能だけで、ドキュメントを読んでいると冒頭で紹介した機能以外にもさまざまな機能があります。 例えば、HTTPプロキシとして振る舞うことができたり、Scrapyと連携したりもできるなど、色々と面白い使い方ができそうです。

良ければ使ってみてください。

参考

*1:Scrapinghub社は、Pythonの有名なスクレイピングフレームワークScrapyの開発者がやっている会社です。