うえださんが通りますよ

CasperJSめも

CaperJSを触り始めたのだが、まとまってる記事がそんなにない。徒然なるメモをここに。

そもそも論

CaperJSはNode.jsのスクレイピングモジュールでありスクレイピングツールである。スクレイピングツールじゃなくてテストツールかも知れないが、スクレイピングに使えるのは間違いない。

また、Node.jsのモジュールなので当然Javascriptで書く。

CasperJSはPhantomJSというヘッドレスブラウザをハンドルする。ヘッドレスって何かというと、GUIがないブラウザ。かといってLynxとかW3mとかみたいなCUIブラウザとも違う。JavaScriptも基本実行してくれたりとレンダリング処理は内部で走るのだが、ユーザに表示する画面を持たない。だから、うまいことPhantomJSをハンドルすれば、「あそこのここをクリックして表示されたページのここんとこのこのテキストを抽出!」とかを自動化できたりする(雑)。

導入

CasperJSはnpmパッケージにある。nodejsとnpmは先に入れておくこと。

Macbrewが入ってるなら下記で両方導入できる。

$ brew install nodejs

ubuntuなどのdebian系でもapt-getで可能だが、npmも指定する必要があったはず(下記)

$ sudo apt-get install nodejs npm

まぁ普通にnode.jsのページからダウンロードするのでもいい。Windowsだとこれがお手軽(というか他は知らない)。

Node.js

node.jsを導入してnpmコマンドが使えるようになったら、 下記コマンドでCasperJSをインストールする。

$ npm install -g casperjs

なお、カレントディレクトリのみにインストールしたい場合は、-gオプションをつけないこと。

PhantomJSの導入

PhantomJSもnpmでインストールできるはず。

$ npm install -g phantomjs

ただ、筆者のlubuntu環境ではうまく行かなかった。(windowsMacでは大丈夫だった) だが、普通にapt-getでインストールできた(下記)。

$ sudo apt-get install phantomjs

一応補足で、PhantomJSは単体でインストールできる。PhantomJSはCasperJSのものではなく、Seleniumなどのスクレイピングツールでも利用される。というかむしろCasperJSがPhantomJSしか使えない。いや、正確にいうと他にも使えるのだが…そこらへんは知識がない。

というかCasperJS使うと、いちいちthenの中にfunctionとか書かなきゃで面倒だし今CasperJSの制御なのかPhantomJSの制御なのか意味不明になるしそもそも2つの間で共通でないObjectとかあったりしてややこしさ極まりないので素直にSeleniumで書くべきだと思う。

げふんげふん。

とりあえず動くもの

習うより慣れろが一番早い習得方法だと思ってる。

はてなブログの、おすすめ記事一覧を取得するサンプルコード(ファイル名sample.js)。

var url = "http://hatenablog.com/";
var useragent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36";

var casper = require('casper').create();

casper.userAgent(useragent);
casper.start(url, function() {
    var ret = casper.evaluate(function() {
        /////////// ここからPhantomJS内でのコンソール実行だと思うといい //////////////
        var recommends = document.querySelectorAll(".serviceTop-entry-title > a");
        return Array.prototype.map.call(recommends, function(self){ return self.text });
        //////////////////////////////////////////////////////////////////////////////
    });

    casper.echo("<<< おすすめ記事一覧 >>>");
    casper.eachThen(ret, function(_ret) {
        casper.echo(_ret.data);
    });
});


casper.run();

実行

$ casperjs sample.js

<<< おすすめ記事一覧 >>>
『友人の家でのえ?』な話
今までで面白かったナンパ、怖かったナンパ厳選まとめ
ZOZOTOWN(ゾゾタウン)がツケ払いOKに!今後は洋服や靴を買ってから最大2ヶ月間、支…
人間の闇を掘り尽くすNHKの「ねほりんぱほりん」が凄い
2016秋クールのアニメ&ドラマがおもしろすぎてやっぱりテレビは最高
涼宮ハルヒ、29歳。
これから先10年、フロントエンドに関する予言
グッド・モーニング
あなたはこの文章を読んで「こいつめんどくせえな」と思う
これから先10年、フロントエンドに関する予言
プログラミング学習日記 PHP編2-3日目 -ゲストブック作成-
PHPカンファレンス2016で発表してきました #phpcon2016
git/githubのマイナーな便利コマンド
風邪をひきやすい時期は、鍋で体を温めよう!
weekend Ramen|牡蠣と四方竹の中華炒めあんかけ醤油ラーメン
砂糖・バター不使用 はちみつヨーグルトケーキ
なすのミートソースグラタン。
システム手帳論考9_【スピンオフ】
男性の失業で起きるお金以外の4つのデメリットとは
「ソフトウェアテスト勉強会~テスターと創る開発現場~」感想
10月のブログ・クラウドソーシングの収入と今後の目標など
小学校高学年に読んでほしい50冊。いや、「子どもと一緒に読みたい本」。
たった4ヶ月でTOEIC350点から日常英会話が出来るようになった英語勉強方法
【お題】 まだヒートテックなんかで冬を過ごしているの?

ざっくり解説

var casper = require('casper').create();

CasperJSのモジュール読み込み。createでコンストラクタ的なことやってくれると思えばいい。

createの引数にユーザエージェントやタイムアウトなどの設定も書ける。例えばユーザエージェントを指定する場合

var casper = require('casper').create(
    {
        userAgetnt : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36");
    }
);

みたいに書ける(ただ、筆者はうまく行かなかったので、サンプルコードでは別途ユーザエージェントを指定した)。

casper.start(url, function() {
    var ret = casper.evaluate(function() {
        /////////// ここからPhantomJS内でのコンソール実行だと思うといい //////////////
        var recommends = document.querySelectorAll(".serviceTop-entry-title > a");
        return Array.prototype.map.call(recommends, function(self){ return self.text });
        //////////////////////////////////////////////////////////////////////////////
    });

    .....

});

casper.startでオープンするurlと、オープンしたあとの処理を記述した関数を渡す。

別のurlを再度オープンしたいときはcasper.thenOpenを使うこと。

casper.evaluateに渡された関数は、PhantomJS内で実行される。途中、わざわざArray.prototypeを駆使してmap関数を利用したが、これは取得したDOM配列(もどき)はArrayオブジェクトではなくNodeListオブジェクトであり、map関数を持たないためである。

(試しにChromeデバッグコンソールでdocument.querySelelctorAll()を実行して返ってきたオブジェクトでmap関数を使うとエラーになる)

casper.evaluate内は基本的にブラウザ側での実行処理になるので、Chromeデバッグコンソールで記述できる制御は、casper.evaluateでも記述できると思っていいはず。細かい仕様の差分はあるかもしれないが。

カンのいい方はお気づきかも知れないが、casper.evaluate内で実行される関数はPhantomJS側で実行されるので、console.log("hogehoge")とか書いても、何も表示されない。これは、Itsukaraさんのブログに細かく記載されている。

itsukara.hateblo.jp

こちらのブログは、CasperJSのデバッグ方法についても細かく書かれているので、ぜひ参考にされてほしい。

casper.evaluate内で最後に

return Array.prototype.map.call(recommends, function(self){ return self.text });

と書いているが、CasperJS側に単純なオブジェクトなら返すことができる。逆に、CasperJS側からPhantomJSにオブジェクトを渡したいとき(グローバル変数だろうとcasper.evaluate内では別の(PhantomJSの)世界での出来事になるので、その変数は参照できなくなる)は、

casper.evaluate(function(_data){
    ...
}, data);

と書けば、data_dataに渡される。

単純なオブジェクトと書いたが、これが地味に厄介である。単純な配列や連想配列程度なら問題なく渡せるのだが、連想配列の値に配列をもつ、など多少複雑になった途端、なんだかよくわからないオブジェクトが渡される。

思いつきで、

return document.querySelectorAll(".serviceTop-entry-title > a");

とか書いてCasperJS側でDOMをごにょごにょやりたい気持ちはとてもわかるが、残念ながらできない。謎のオブジェクトが返ってきて、持て余す。

ただこれは、自分で定義したオブジェクトくらいなら、後述する裏技で無理やり回避しようと思えばできないこともない。

    casper.echo("<<< おすすめ記事一覧 >>>");
    casper.eachThen(ret, function(_ret) {
        casper.echo(_ret.data);
    });

この出力部分で注意する必要があるのは、下記のように書くと出力されないことである。

    casper.echo("<<< おすすめ記事一覧 >>>");
    for(var idx in ret) {
        casper.echo(ret[idx]); // 何も出力されない\(^o^)/
    }

正直細かい理由はわかってないが、for文で書くと、CasperJSとは同期しない何かで動くらしい(すごく曖昧)。

casper.run();

これまで書いてきたcasper.start内の記述は、言ってみればプログラムの実行計画部分で、実際に走査処理(スクレイピング処理)が行われるのは、casper.run()してからである。これを書かないと、何もおきない。

【おまけ】CasperJSとPhantomJS間のオブジェクトの受け渡し

前述したように、多少複雑なオブジェクトはCasperJSとPhantomJS間で受け渡しがそのままできない。

これは会社の同僚のひらめきから生まれた裏技なのだが、いっそJSON文字列にして受け渡してしまえば、なんとかなったりする。

具体的には、casper.evaluate内でretObjを返すならreturn JSON.stringify(retObj)すればいい。

受け取った側はJSON.parseでもとのオブジェクト(にそっくりなもの)を復元できる。

ただ、これはDOMオブジェクトほど複雑になるとうまくJSON文字列化できなかったので、限界がある。 藁にもすがりたいときの藁程度の知識として、一応知っておくといいと思う。