古くて新しいWEBスクレイピング技術ですが、最近では「ロボティクス」や「ヘッドレスブラウザ」という言い方で、仕事でインターネットの特定のサイトの情報を取得して、エクセル資料にコピペする作業をしているのを自動化するシステムやツールが大変ニーズがあるそうです。
その裏側の技術はほとんどがスクレイピング技術を使って行なっているので、呼び名が変わっているだけで、やってることはほとんど一緒であるとも言えます。
そんな中、最近のスクレイピングは一昔前と違って、ちゃんとjavascript動作を考慮して画面レンダリングが行える事が最低条件にもなっているため、ハードルはどんどん上がっている感じです。
そんな中、WEBレンダリングが手軽に行える
phantomjsと、ページ遷移などを行う機能が豊富な
casperjsを組み合わせた、「
SpookyJS」が、内部にjavascriptでも記述ができて、両方をwrapした機能網羅性により、このツール以外が一番使いやすいと感じています。
ただし、デメリットとしては、ページ遷移する際に毎回レンダリングを行うので、スピード重視のスクレイピングには向いていないかもしれませんが、多くの場合にBANされるのを回避するために、ページ読み込みにwaitを掛けるのが一般的なので、これはこれで良し!
Spookyjsでのつまずきポイント
spookyjsの設定は以前書いた記事を参考にしてください。
[Nodejs] クローリングシステム構築 #1.Spookyjsの環境設定
色々なサイトで解説されていますが、ページ内でリンク一覧を取得して、不確定な個数のリンクに遷移して、その先のページをスクレイピングするような場合、spookyjsは特有な内部変数を処理しなければならないため、その構文と値の受け渡しに注意しなければいけません。
spookyjsフロー
spookyjsの一般的な記述フローは以下の通りです。
spooky.start("%url");
spooky.then(function(){
var response = this.evaluate(function(){
return location.href;
});
this.emit("log" , response);
this.exit();
});
spooky.on("log",function(response){
console.log(response);
});
spooky.run();
かなり複雑に入り組んでいますが、基本的にコールバック形式なので、それぞれの処理順番などを理解しておかなければいけません。
上記のフローと、それぞれの解説は、以下の様な進行になります。
1. "spooky.start"、任意のURLを読み込む
2. "spooky.then"、ページ遷移する度にthen処理を実行するというのがわかりやすいです。
3. "this.evaluate"、読み込まれたページの処理をjavascriptで記述でき、retuenで結果の値を受け取れます。
4. "this.emit"、取得した値や任意のデータをonでセットされたイベント関数に送る。
5. "this.exit"、処理の終了にはexit処理を書かないとspooky処理から抜け出せなくなります。
6. "spooky.on"、emitで呼び出される処理(コールバック内はグローバル処理で書きます)
7. "spooky.run"、上記セットされたプログラムを実行します。
exitが中盤に書かれているので、気持ち悪い人もいるかもしれませんが、その場合は、最後にthenを追加して、その中でexitしてあげても大丈夫です。
変数の受け渡し
上記のプログラムのままでは、あらかじめ決まっているグローバル変数などをレンダリングされたWEBページに引き継ぐ事ができません。
ちゃんと引数を送ってあげる必要があります。
具体的には、レンダリングされたページ内の特定のselectorの情報を取得したい場合、通常であれば、evaluateのコールバックに任意に記述してしまってもいいのですが、システム管理的に外部設定でselectorの値を任意に変える様な場合、グローバル変数で値を読み込み、evaluateに受け渡さなければいけません。
その場合は以下の様に記述できます。
var selector = "#hoge a:nth-child(1)";//ID="hoge"内にある最初のAリンク
spooky.start("%url");
spooky.then([ {selector : selector} , function(){
var response = this.evaluate(function(selector){
var elm = document.querySelector(selector);
return elm.href;
} , {selector : selector});
this.emit("log" , response);
this.exit();
}]);
spooky.on("log",function(response){
console.log(response);
});
spooky.run();
thenの受け渡しが以下の様に変わりました。
before : spooky.then(function(){...});
after : spooky.then([ {key : value} , function(key){...} ]);
// コールバック内でkey変数で利用できます。
さらにその値をevaluateに受け渡すのに以下の記述です。
this.evaluate(function(){...});
this.evaluate(function(key){...} , {key:value});
// コールバック内でkey変数で利用できます。
受け渡す形式がそれぞれ違うので、非常にめんどくさいですね。
この辺に慣れる事が重要です。
ちなみに、thenの変わりに、eachThenという記述もできて、thenコールバック内でthenを引き渡し値で利用したい場合は、この関数で行うのが安全です。(何故かこの方式以外はエラーになりがちです)
その場合の記述は以下のとおりです。
before : this.eachThen(function(){...});
after : this.eachThen({key : value} , function(response){...});
// コールバック内では、response.data.key変数で利用できます。
さらに違った形式での記述方法で、且つ、変数もオブジェクトで受け渡されます。
ここまでくると「慣れろ!!」です。
複数のURLを遷移したい場合
最初のページで取得したURL一覧を元に、それぞれのページに移動してスクレイピングしたい場合のサンプルです。
spooky.start("%url");
spooky.then(function(){
// URL一覧の取得
var links = this.evaluate(function(){
var elm = document.links;
return JSON.stringify(elm);
});
// URL別処理
for(var i=0; i<links .length; i++){
this.open(links[i]);
var response = this.evaluate(function(){
return document.title;
});
this.emit("log" , response);
}
this.exit();
});
spooky.on("log",function(response){
console.log(response);
});
spooky.run();
取得したURL一覧をfor文で回していますが、実はこの状態では、同じページの結果しか得られません。
ページ遷移する時はthenを分けなければいけません。
でも、ここでもうひとつのつまずきポイントがあり、thenの中で処理された変数は、thenの外側に引き継げないんですね。
コールバックの宿命ですが、evaluateだけ例外だと考えましょう。
これを解決するには以下の様に記述できます。
spooky.start("%url");
spooky.then(function(){
// URL一覧の取得
var links = this.evaluate(function(){
var elm = document.links;
return JSON.stringify(elm);
});
// URL別処理
for(var i=0; i<links .length; i++){
this.eachThen({url:links[i]} , function(val){
this.open(val.data.url);
});
this.eachThen(function(){
var response = this.evaluate(function(){
return document.title;
});
this.emit("log" , response);
});
}
this.exit();
});
spooky.on("log",function(response){
console.log(response);
});
spooky.run();
複雑になりましたが、比べて違いを把握してください。
this.open()とthis.evaluate()をthis.eachThen()内部にそれぞれ記述してthen処理を設けました。
これにより、ちゃんと値を引き継いでURL遷移処理も行えます。
ひょっとしてもっとシンプルに書けるのだろうか?
今の所僕の技術レベルでは、このやり方が一番しっくりきて汎用性が高いので、オススメなんですけどね。
関連リンク
クローリングシステム構築
0 件のコメント:
コメントを投稿