凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Node.js × PhantomJS で何でもサクサクスクレイピングするよ!

はじめに

先日、PhantomJS でログインが必要なページでも自由自在にスクレイピング - 凹みTips という記事を書きました。
前回は PhantomJS のみを用いてスクレイピングを行なっていましたが、スクレイピングした結果を使って色々やりたい!となると、Node.js の力を借りたくなってきます。単純に取得した HTML を利用するだけなら、PhantomJS の fs モジュールを使って HTML をファイルに書き出し、これを Node.js で読み取って…、なんてことをすれば可能ですが、何かしらの入力を受けて動的にページを遷移したい、となると厳しくなってきます。
そこで、本エントリでは PhantomJS を Node.js から使って色々出来るよ!ということを解説したいと思います。

利用するモジュール

PhantomJS をラップした Node モジュールは沢山あります。試しに、

$ npm search phantomjs

すると、大量に出てくると思います。色々試してみましたが、「phantomjs-node」というモジュールが上手く動いたので今回はコレを使ってみます。またパースには cheerio を使用しました。

$ npm install phantom cheerio

コード

例として、はてなにログインし、自分のユーザ名とはてなポイントの残りを取得してみます。基本的な流れは前回と同じです。

コード
var phantom = require('phantom');
var cheerio = require('cheerio');

phantom.create(function(ph) {
	ph.createPage(function(page) {

// ページが読み込まれたら page.onCallback を呼ぶ
page.set('onInitialized', function() {
	page.evaluate(function() {
		document.addEventListener('DOMContentLoaded', function() {
			window.callPhantom('DOMContentLoaded');
		}, false);
	});
});

// ページが読み込まれたら登録した関数の配列を順次実行してくれるクラス
var funcs = function(funcs) {
	this.funcs = funcs;
	this.init();
};
funcs.prototype = {
	// ページが読み込まれたら next() を呼ぶ
	init: function() {
		var self = this;
		page.set('onCallback', function(data) {
			if (data === 'DOMContentLoaded') self.next();
		});
	},
	// 登録した関数の配列から1個取り出して実行
	next: function() {
		var func = this.funcs.shift();
		if (func !== undefined) {
			func();
		} else {
			page.set('onCallback', undefined);
		}
	}
};

// 順次実行する関数
new funcs([
	function() {
		console.log('ログイン処理');
		page.open('https://www.hatena.ne.jp/login'); // 次ページヘ
	},
	function() {
		console.log('ログイン画面');
		page.evaluate(function() {
			document.getElementById('login-name').value = 'はてなの ID';
			document.querySelector('.password').value   = 'パスワード';
			document.querySelector('form').submit(); // 次ページヘ
		});
	},
	function() {
		console.log('ログイン後画面');
		setTimeout(function() {
			page.open('http://www.hatena.ne.jp/my');
		}, 2000);
	},
	function() {
		console.log('iframe 内');

		// iframe 内の HTML を取得
		page.evaluate(function() {
			return document.getElementsByTagName('html')[0].innerHTML;
		}, function(html) {
			// cheerio でパースしてユーザ名とポイントを取得
			var $ = cheerio.load(html);
			var point = $('.hatena-module').eq(0).find('.count').text();
			console.log('ポイントは後', point, 'point だよ!');

			// お忘れなきよう (-人-)
			ph.exit();
		});
	}
]).next();

	});
});
結果

純粋な PhantomJS と違って evaluate した結果はコールバックで受け取ったり、page のプロパティには set/get でアクセスするといった差異があるのでご注意を。
あと純粋な PhantomJS の時と違ってログイン中画面の遷移が上手く行かなかったので泣く泣く setTimeout で待機する処理使ってます( ;∀;)。
ちなみに phantom-sync なんていう phantom をラップしたモジュールもあるので、こちらを用いたコードは以下の gist にアップしました。

終わりに

ZombieJS なんていう Headless ブラウザを Node.js から使えるモジュールもありますが、WebKit で動いてくれる PhantomJS の方が安定感あるかなー、と思います(ZombieJS ではてなへのログイン出来なかったので…)。
Node.js x PhantomJS で快適なスクレイピングライフを送って下さい!