JavaScript
PhantomJS

はじめに

この記事には真新しい内容はありません、スクレイピングを手軽に、言語に縛られないように僕がPhantomJSをどう利用しているかの備忘録的な位置づけになります。

やりたい事

スクレイピングに集中したい

スクレイピングでやりたい事はDOMの収集です、必要ならばJavaScriptの処理実行後に収集します、動的なぺージ遷移、POSTによるページ遷移も行います、これらの煩わしい処理はPhantomJSに任せ、スクレイピング以外のコードをなるべく書かないように済ませられるようにします。

どの言語でも扱えるように汎用化しておきたい

各種言語で各種ライブラリが日進月歩で登場しています、必要だと思われたから登場したのでしょうが言語を変える度に最新のライブラリを追って組み込むのは大変です。
JavaScriptでスクレイピングし、結果をJSONで返す部分を切り出せば、各種言語はその処理を呼び出し、JSONのパーサーを実装するだけで良くなります。

って事で本題。

JavaScriptでスクレイピング、動的なページ遷移を行い、結果をJSONで表示する

スクレイピングに集中するために、その他の雑多の処理を隠蔽するためのPhantomJSラッパーライブラリを作成しました。
wordijp/zakutohatigau(GitHub)

最初は単なるラッパーだったのですが、この記事を書くにあたり名前があった方が呼びやすいと思い、

DOMに集中したい → DOM → ドム → ザクとは違う → zakutohatigau

というこじつけで名づけました。
使い方は、初期設定と開始用のrun関数の実行以外は、スクレイピング処理をentry関数に登録するだけとなります。

こちらを使い、Qiitaからスクレイピングの単語が含まれる記事のURL一覧をJSONで表示するコードは下記のようになります。

example/qiita.js
const Dom = require('../zakutohatigau').Zakutohatigau;

const dom = new Dom({
  url: 'https://qiita.com/search',
  // サイト側でjQueryを使っていてもぶつからないように、
  // _を先頭に付けて改造したjQueryを追加で読み込ませる
  injectJs: ['./lib/_jquery-3.2.1.min.js'],
  // ページ遷移間隔
  interval: 1000,
});

// 検索画面
dom.entry(function (jsonobj, page) {
  jsonobj['here 1'] = 'from search page';

  // 検索単語をセット
  page.evaluate(function() {
    _$('input[id=q]').val('スクレイピング');
  });

  // ページ遷移をする場合は、ページ遷移用関数を返す
  return function onNextPage () {
    // 検索
    page.evaluate(function() {
      _$("button[type=submit]").click();
    });
  };
});

// 検索結果画面
dom.entry(function (jsonobj, page) {
  jsonobj['here 2'] = 'from result page';

  // 検索結果の一覧を収集する
  var list = page.evaluate(function() {
    return _$('.searchResult_itemTitle')
      .find('a[href]')
      .map(function() {
        var $self = _$(this);
        return {
          title: $self.text(),
          href: $self.attr('href'),
        };
      })
      .get();
  });

  jsonobj['list'] = list;
});

dom.run();

この時の実行結果は次のようになります。
phantomjsはnpmか実行バイナリをインストールしておきます。

$ phantomjs example/qiita.js | jq .
{
  "here 1": "from search page",
  "here 2": "from result page",
  "list": [
    {
      "href": "/aoi_shima/items/0cd39692f071b059244e",
      "title": "pythonによるスクレイピング    "
    },
    {
      "href": "/h5y1m141@github/items/118571c94ca42d2daf6a",
      "title": "Webサイトをスクレイピングする時のちょっとしたコツ"
    },
    {
      "href": "/tukiyo3/items/189c990e25436b06d9b4",
      "title": "cheerio-httpcliで俺の勉カレをスクレイピングしてみた"
    },
    {
      "href": "/tomi_shinwatec/items/a68cf7840c3da002c6e0",
      "title": "puppeteerでスクレイピング"
    },
    {
      "href": "/yyano/items/b76568bdb3a0be3ff91e",
      "title": "Wgetしてスクレイピングする"
    },
    {
      "href": "/takahiroSakamoto/items/c2b269c07e15a04f5861",
      "title": "Javaでスクレイピングをしよう!!"
    },
    {
      "href": "/taroyanaka/items/1662ede7938a614af149",
      "title": "食べログのWebページから店舗のURLと点数を抽出するrubyプログラム"
    },
    {
      "href": "/uyuutosa/items/8af33f3abd0d9d8b13fe",
      "title": "画像のスクレイピング"
    },
    {
      "href": "/mo4_9/items/c4aa153d3a176eaafacc",
      "title": "スクレイピングのサンプル"
    },
    {
      "href": "/hyonsoku/items/a02ea1d74fb88113a7e5",
      "title": "RでJリーグの結果をスクレイピングする"
    }
  ]
}

zakutohatigauを作った経緯

PhantomJSのラッパーライブラリとしてはすでにCasperJSがあったのですが、動的にページ遷移後の処理を追加する関数が見当たらず、JSON化の処理も必要であった為、余計なライブラリを挟むよりは純正のPhantomJSに薄いラッパーを被せただけの方がシンプルで済むと思いこのようにしました。
それにCasperJSにはダウンロード関数やクリッピング指定のスクリーンショットをする関数などが用意されてはいますが、それらのJavaScriptコードを確認したところPhantomJSのラッパーであった為、MITライセンスという事もあり必要になればコードの移植すればいいかと思ったのも理由です。

終わりに

PhantomJSにも問題があります、それは遅いこと、これに尽きます。
丁度4日目の記事にてsplashが紹介されていたので、こちらを利用出来れば乗り換えたいです。