はじめに
PhantomJS はヘッドレスな(ブラウザ画面のない)QtWebKit ベースのブラウザで、JavaScript の API を通じて、そのブラウザを自由自在にあやつることが出来ます。使用シーンとしては、Jenkins などの CI ツールとの組み合わせによる Web ページの GUI の自動テストや、Web ページのスクリーンキャプチャ、スクレイピングなどが挙げられます。
今回は、ログインが必要なページの情報をパースして自分専用に RSS 化したいなと思い、3つ目のスクレイピング用途をベースに調べてみました。その内容を備忘録として残しておきます。
出来るようになること
- ログインが必要なページの HTML を取ってくる
- ログインが必要なページのスクリーンキャプチャを撮る
PhantomJS の基礎
ブラウザの起動とデータの取得とスクリーンキャプチャ
ブラウザを作ってタイトルを調べてみます。
// Headless ブラウザの生成 var page = require('webpage').create(); // URL を開く page.open('http://www.google.co.jp', function(status) { if (status === 'success') { // スクリーンキャプチャ page.render('google.png'); // ブラウザ内で JS を実行してデータを受け取る var title = page.evaluate(function() { var title = document.title; return title; }); console.log(title); } // exit しないと終了しない phantom.exit(); });
$ phantomjs hoge.js Google
キャプチャ画像:
page.open() で URL を開いて、コールバック内で page.evaluate() をし、その中で JavaScript コンソールを使っているような気分で JS を実行、return するとそのデータが外側に返ってくる、という世界観です。
外部 JS の利用
DOM 操作も jQuery を利用できたほうが楽ですよね。includeJs を使うと外部の JS を読み込んで、それを evaluate 内で使用することができます。
// Headless ブラウザの生成 var page = require('webpage').create(); // URL を開く page.open('http://www.google.co.jp', function(status) { // jQuery を使う page.includeJs('http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js', function() { var title = page.evaluate(function() { var title = $('title').text(); return title; }); console.log(title); // Google phantom.exit(); }); });
evaluate 外の変数を利用
evaluate 内に外の世界の JS の変数を持ち込みたい場合は外部 JS を読みこませれば良いようです。
ページの遷移
ページを遷移するには以下のようにすれば良いようです。
コードを抜粋させていただくと、以下のようになります。
var page = require('webpage').create(); var funcs = [ function(){ page.open("http://www.p-rex.net/c15/"); }, function(){ page.render('1.jpg'); page.open("http://www.p-rex.net/c17/"); }, function(){ page.render('2.jpg'); page.open("http://www.p-rex.net/c23/"); }, function(){ page.render('3.jpg'); page.open("http://www.p-rex.net/c36/"); }, function(){ page.render('4.jpg'); } ]; var recursive = function(i){ if(i < funcs.length){ page.onLoadFinished = function(){recursive(i+1);}; funcs[i](); }else{ phantom.exit(); } }; recursive(0);
ここでは page.onLoadFinished にコールバックを登録しておき、ページが読み終わったら順に呼ぶようにしているようです。
しかしながら、onLoadFinished は iframe の読み込み終了時にも呼ばれます、つまり iframe を 10 個使っているページでは 11 回(自分自身 + iframe)呼ばれてしまうことになります。
そこで調べてみたところ、以下のページにて解決法が議論されていました。
gist に貼られたコードを抜粋すると以下のようになります。
var page = require('webpage').create(); page.onInitialized = function() { page.evaluate(function(domContentLoadedMsg) { document.addEventListener('DOMContentLoaded', function() { window.callPhantom('DOMContentLoaded'); }, false); }); }; page.onCallback = function(data) { // your code here console.log('DOMContentLoaded'); phantom.exit(0); }; page.open('http://phantomjs.org/');
PhantomJS では window オブジェクトに callPhantom というメソッドが追加されています。これを呼ぶと外側の世界の page.onCallback を引数を伴って呼ぶことができます。そこでブラウザ側の DOMContentLoaded イベントハンドラをフックしてサーバ側で呼ばれる onCallback を onLoadFinished コールバックの代わりに利用することで、複数回呼ばれてしまう問題を解決しています。
そこで、これを利用して新しいページが読み込まれ次第、登録した関数が順々に実行されていく処理を書いてみました。内容としては、hatena にログインしてその画面をスクリーンキャプチャ、更に HTML を取得したものになっています。
var page = require('webpage').create(); var fs = require('fs'); // ページが読み込まれたら page.onCallback を呼ぶ page.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.onCallback = function(data){ if (data === 'DOMContentLoaded') self.next(); } }, // 登録した関数の配列から1個取り出して実行 next: function() { var func = this.funcs.shift(); if (func !== undefined) { func(); } else { page.onCallback = function(){}; } } }; // 順次実行する関数 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('ログイン中画面'); // 自動で次ページヘ }, function() { console.log('ログイン後画面'); page.render('mypage.png'); // ログイン後の HTML を書き出し var html = page.evaluate(function() { return document.getElementsByTagName('html')[0].innerHTML; }); fs.write('mypage.html', html, 'w'); phantom.exit(); } ]).next();
fs モジュールを使うとファイル書き出しも出来るので、Node.js との連携も簡単ですね。