orangain flavor

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

PhantomJSとか使わずに簡単なJavaScriptを処理してスクレイピング

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

JavaScriptが使われているWebページからスクレイピングする場合、PhantomJSなどのヘッドレスブラウザーを使うのが一般的です。 ただ、ちょっとしたJavaScriptを解釈できれば十分な場合、オーバーキルなこともあります。

この記事では、PhantomJSとかを使わずに簡単なJavaScriptを処理する方法を解説します。

どんな場合に役立つの?

NHKニュースのWebサイトを題材として取り上げます。

NHK NEWS WEBのRSSではニュースのタイトルとURL、概要しか提供されていません。画像のURLやニュースの本文を取得したい場合は、スクレイピングが必要です。

普通にスクレイピングしてもいいですが、ソースを見るとJavaScript__DetailProp__という変数が定義されています。この変数の値のオブジェクトをパースできればXPathCSSセレクターでパースするより楽ですし、変更に強くなりそうです。

<script type="text/javascript">
   var __DetailProp__ = {
       cate: '5',
       date: '12月15日 4時04分',
       datetime: '2016-12-15T04:04',
       //id: 'k10010807361000',
       //video: 'k10010807361_201612150440_201612150458.mp4',
       video: 'https:\/\/www3.nhk.or.jp\/news\/html\/20161215\/movie\/k10010807361_201612150440_201612150458.html',
       duration: 87,
       img: 'html\/20161215\/K10010807361_1612150440_1612150458_01_03.jpg',
       title: '\u7C73\uFF26\uFF32\uFF22 \u8FFD\u52A0\u5229\u4E0A\u3052\u6C7A\u5B9A \uFF11\u5E74\u3076\u308A',
       summary: '\u30A2\u30E1\u30EA\u30AB\u306E\u4E2D\u592E\u9280\u884C\u306B\u5F53\u305F\u308B\uFF26\uFF32\uFF22\uFF1D\u9023\u90A6\u6E96\u5099\u5236\u5EA6\u7406\u4E8B\u4F1A\u306F\u3001\uFF11\uFF14\u65E5\u307E\u3067\u958B\u3044\u305F\u91D1\u878D\u653F\u7B56\u3092\u6C7A\u3081\u308B\u4F1A\u5408\u3067\u3001\u30A2\u30E1\u30EA\u30AB\u7D4C\u6E08\u306F\u62E1\u5927\u3057\u3066\u3044...',
       more: '\uFF26\uFF32\uFF22\u306F\uFF11\uFF14\u65E5\u307E\u3067\u306E\uFF12\u65E5\u9593\u3001\u30EF\u30B7\u30F3\u30C8\u30F3\u3067\u91D1\u878D\u653F\u7B56\u3092\u6C7A\u3081\u308B\u516C\u958B\u5E02\u5834\u59D4\u54E1\u4F1A\u3092\u958B\u304D\u307E\u3057\u305F\u3002<br /><br />\u7D42\u4E86\u5F8C\u767A\u8868\u3055\u308C\u305F\u58F0\u660E\u306B\u3088\u308A\u307E\u3059\u3068\u3001\uFF26\uFF32\uFF22\u306F\u3001\u30A2\u30E1\u30EA\u30AB\u7D4C\u6E08\u306B\u3064\u3044\u3066\u3001\u96C7...',
       body: [{
           title: '\u4ECA\u5F8C\u306E\u91D1\u5229\u306E\u898B\u901A\u3057\u306F',
           img: '',
           align: '',
           text: '\uFF26\uFF32\uFF22\u306F\uFF11\uFF14\u65E5\u3001\u30A4\u30A8\u30EC\u30F3\u8B70\u9577\u306A\u3069\u91D1\u878D\u653F\u7B56\u3092\u6C7A\u3081\u308B\u4F1A\u5408\u306E\u53C2\u52A0\u8005\u304C\u4E88\u6E2C\u3057\u305F\u3001\u4ECA\u5F8C\u306E\u91D1\u5229\u306E\u898B\u901A\u3057\u3092\u516C\u8868\u3057\u307E\u3057\u305F\u3002<br /><br />\u305D\u308C\u306B\u3088\u308A\u307E\u3059\u3068\u3001\u6765\u5E74\u306E\u5229\u4E0A\u3052\u306E\u56DE\u6570\u306E\u4E2D\u5FC3\u7684\u306A\u60F3\u5B9A\u306F\uFF13\u56DE\u3068\u3001\uFF19\u6708\u306E\u6BB5\u968E\u306E\uFF12\u56DE\u3088\u308A\u3082\u5897\u3084\u3057\u3066\u3044\u3066\u3001\u30C8\u30E9\u30F3\u30D7\u6C0F\u306E\u7D4C\u6E08\u653F\u7B56\u306B...',
           textPos: ''
       },{
           title: '\u300C\u30BC\u30ED\u91D1\u5229\u653F\u7B56\u89E3\u9664\u300D\u304B\u3089\u306E\uFF11\u5E74\u306F',
           img: '',
           align: '',
           text: '\uFF26\uFF32\uFF22\u306F\u53BB\u5E74\uFF11\uFF12\u6708\u3001\u3044\u308F\u3086\u308B\u30EA\u30FC\u30DE\u30F3\u30B7\u30E7\u30C3\u30AF\u306E\u3042\u3068\u5C0E\u5165\u3057\u3066\u3044\u305F\u300C\u30BC\u30ED\u91D1\u5229\u653F\u7B56\u300D\u3092\u3001\u96C7\u7528\u306E\u6539\u5584\u306A\u3069\u3092\u7406\u7531\u306B\u89E3\u9664\u3059\u308B\u3053\u3068\u3092...',
           textPos: ''
       }]
   };
</script>

JavaScript以外の言語でこのオブジェクトをパースするのは少し面倒なので、JavaScriptエンジンを使います。

組み込みJavaScriptエンジン:Duktape

この程度のオブジェクトをパースするのであれば、ブラウザーの機能は不要でJavaScriptエンジンが動けば十分です。 幸い、いろいろな言語から使える組み込みのJavaScriptエンジンはいくつかあり、ここではDuktapeを取り上げます。

Duktape

真面目に比較検討したわけではないですが、ポータビリティの高さとフットプリントの小ささが特徴のようです。 いろいろな言語のバインディングがあります。

PythonでもPyPIduktapeで検索すると、以下のライブラリが見つかります。

ここでは一番よくメンテナンスされている感じのdukpyを使用します*1

動かしてみる

環境の準備

Python 3.5.1を使い、pipでdukpyをインストールします。バージョン0.0.6を使います。

(venv) $ pip install dukpy

ソースコード

以下のようなコードでスクレイピングできます。script要素内のJavaScriptのコードを取得し、dukpy.evaljs()で実行します。

nhknews.py

import urllib
import re

import dukpy

# Webページを取得する。
f = urllib.request.urlopen('http://www3.nhk.or.jp/news/html/20161215/k10010807361000.html')

# HTMLのボディをUnicode文字列にデコードする。
html = f.read().decode('utf-8')

# 正規表現でscript要素の中身を取得する。
# re.DOTALLで正規表現中の.が改行にもマッチするようになる。
m = re.search(r'<script type="text/javascript">(.*?)</script>', html, re.DOTALL)

# 正規表現でキャプチャしたscript要素の中身を取得する。
script = m.group(1)

# dukpyでJavaScriptを実行し、変数__DetailProp__の値を取得する。
js_obj = dukpy.evaljs([script, '__DetailProp__'])

# 取得できた値(タイトル、画像のURL、もっと読むの中身)を表示する。
print(js_obj['title'])  # タイトル
print(js_obj['img'])    # 画像のURL(/news/からの相対URL)
print(js_obj['more'])   # もっと読むの中身

実行

実行すると次のようになり、JavaScriptの変数に格納されていた値を取得できていることがわかります。

(venv) $ python nhknews.py
米FRB 追加利上げ決定 1年ぶり
html/20161215/K10010807361_1612150440_1612150458_01_03.jpg
FRBは14日までの2日間、ワシントンで金融政策を決める公開市場委員会を開きました。<br /><br />終了後発表された声明によりますと、FRBは、アメリカ経済について、雇用の伸びなどを背景に緩やかなペースで拡大しているとして、全会一致で政策金利を引き上げることを決めました。<br /><br />具体的には、0.25%から0.5%の範囲となっている今の政策金利を、0.25%引き上げて、0.5%から0.75%の範囲にします。<br /><br />FRBが利上げに踏み切るのは、去年12月にゼロ金利政策を解除して以来、1年ぶりとなります。...

解説

dukpy.evaljs()関数には、JavaScriptのコードを文字列、または文字列のリストとして渡します。文字列のリストを渡した場合は、同じコンテキストで順に実行されます*2。戻り値は、最後に評価された値です。

今回の場合、script要素の中身はvar文だけなので、そのままだと評価される値はundefinedPythonではNone)となります。このため、リストの2つ目の要素として変数名を与えることで、変数__DetailProp__を評価し、その値を得ます。

JavaScriptのオブジェクトはPythondictにいい感じに変換されます。 dukpyの詳しい使い方はドキュメントを参照してください。

まとめ

Duktapeを使うといろいろな言語から簡単にJavaScriptのコードを実行できます。 Ajaxのようなブラウザーに依存する機能は使えませんが、PlainなJavaScriptを実行できれば十分な場合には便利です。

正直JavaScriptの変数をサーバーサイドで書き出すのはバッドプラクティス*3なので、使える場面は少ないかもしれません。 しかし、こういう手段も知っておくと役に立つ日が来るかもしれません。

宣伝

Pythonクローリング&スクレイピングという本を書きました。紙版・電子書籍版共にちょうど本日発売でした。 この本の中では、オーソドックスにSeleniumとPhantomJSを使ってJavaScriptを使ったページに対応する方法を解説しています。

*1:これはProjects using Duktape (alphabetical order)からリンクされているkovidgoyal/dukpyとは別物です。kovidgoyal/dukpyはPyPIには登録されていないようです。

*2:リストの要素をセミコロン区切りで接続した文字列を実行するのと同等の結果になります。

*3:参考: HTMLのscriptタグ内に出力されるJavaScriptのエスケープ処理に起因するXSSがとても多い件について - 金利0無利息キャッシング – キャッシングできます - subtech