[SpookyJS] 同一ページで複数ダウンロードする時は"eachThen"を使うと便利

2018/11/02

テクノロジー 日記

t f B! P L
クローリングをやっていると色々なホームページがある事に気がつきます。 昔ながらのtableタグを連発しているサイトだったり、bootstrapを使ったdivネスト連発しているサイト、 属性をほとんど付与していないネイティブタグなサイト。 スクレイピング操作をしていると、どのサイトでも鉄板的にデータ取得できるという統一ルールなどない事がよくわかります。 そして最ももどかしいのが、同じサイトの別ページでルールが全く違う(おそらく全く別の制作者)場合にクローリングの手間が数倍かかる事です。 そして今回は、1ページ内にダウンロードボタンが沢山あるページで、データダウンロードを一気に行う方法です。 サンプルページとして、東京都教育員会の資料ダウンロード一覧ページのPDFデータを一括ダウンロードしてみたいと思います。 東京都教育委員会 : 手続一覧・申請様式・事務処理フロー等

ダメパターン

通常考えると、ダウンロードボタンを"querySelectorAll"で一括取得して、"for"で回しながらelement.click()を実行していけばいいと思うのですが、少数であれば、この処理でうまくいく事もあるかもしれませんが、僕が試した結果、これではどうもうまくダウンロードされないという事がわかりました。 var Spooky = require('spooky'); var spooky = new Spooky({ child: { command : '/usr/local/bin/casperjs', transport : 'stdio' }, casper: { logLevel : 'info', verbose : true, waitTimeout : 3000 }} , function(err){ spooky.start(); spooky.thenOpen("http://www.kyoiku.metro.tokyo.jp/consulting/procedure_and_form/format.html"); spooky.then(function(){ var res = this.evaluate(function(){ // PDFダウンロードボタン一覧の取得 var links = document.querySelectorAll(".icon_pdf"); // 一括ダウンロード押下処理 var res = ""; for(var i=0; i<links.length; i++){ links[i].push(); } return res; }); this.emit('message' , res); }); spooky.run(); spooky.then(function(){this.exit();}); }); spooky.on('message', function (res){ console.log("{'message' : '"+res+"'}"); }); // データ受信処理 spooky.on('resource.requested', function(requestData, networkRequest) { if(!requestData.url.match(/\.pdf$/)){return;} console.log("+download : " + requestData.url); }); このプログラムを実行しても、click()をしているにもかかわらずpdfファイルへのリクエストも実行されません。 casperjsとspookyjs(中身は同じです)では、こうした場合、thenやevaluateを一旦終了してもう一度実行する事でアクションが次に進むという特性があるので、"for"文で一気に.click()処理を行なっても空振りするだけという事がわかりました。

eachThenを使ったダウンロード

まずダウンロードする要素一覧を取得してから、それをeachThenで繰り返し処理を行う事で、複数のevaluateに切り分けたダウンロードを行う事ができ、全てのダウンロードが正常に行われるようになりました。 ポイントは、要素をユニーク文字列にする事ですが、これは以前に記事で書いた「ライブラリ「DOMコード」」をそのまま使う事で対応できます。 [JavaScript] ライブラリ「DOMコード」 // dom2text var $$uniqueDom = (function(){ var $$={}; $$.encode = function(elm){ if(typeof(elm)=='undefined' || elm==null || !elm){return ""} var dom = []; do{ if(elm.id && elm == document.getElementById(elm.id)){ dom[dom.length] = elm.id; break; } else if(!elm.parentNode){break} var num = 0; var cnt = 0; if(elm.parentNode.childNodes.length){ for(var i=0;i<elm.parentNode.childNodes.length;i++){ if(typeof(elm.parentNode.childNodes[i].tagName)=='undefined'){continue} if(elm.parentNode.childNodes[i].tagName != elm.tagName){continue} if(elm.parentNode.childNodes[i] == elm){ num=cnt; break; } cnt++; } } //小文字英数字で形成する dom[dom.length] = elm.tagName.toLowerCase() + "["+num+"]"; if(elm == document.body){break} } while (elm = elm.parentNode); //配列を逆にする var dom2 = []; for(var i=dom.length-1;i>=0;i--){ dom2[dom2.length] = dom[i]; } return dom2.join("."); }; $$.decode = function(uid){ if(!uid || typeof(uid)!='string'){return} //単一IDの場合; if(document.getElementById(uid)!=null){return document.getElementById(uid);} //element抽出処理 var elm= document.getElementsByTagName("html")[0]; var d1 = uid.split("."); var flg=0; var path = []; for(var i=0;i<d1.length;i++){ if(d1[i].match(/^(.*?)\[(.*?)\]$/)){ var tag = RegExp.$1; var num = RegExp.$2; if(tag=='' || num==''){return;} var cnt = 0; var flg2= 0; var e2 = elm.childNodes; for(var j=0;j<e2.length;j++){ if(!e2[j].tagName || typeof(e2[j])=='undefined'){continue;} if(e2[j].tagName != tag.toUpperCase()){continue;} if(cnt == num){ elm = e2[j]; flg2++; break; } cnt++; } //存在しないelement処理 if(flg2==0){return;} flg++; } else if(document.getElementById(d1[i])!=null){ elm = document.getElementById(d1[i]); flg++; } else if(document.getElementById(d1[i])==null){ return; } //table-cell用処理 path[path.length] = d1[i]; } if(!flg){return} return elm; }; return $$; })(); var Spooky = require('spooky'); var request = require("request"); var fs = require('fs'); var spooky = new Spooky({ child: { command : '/usr/local/bin/casperjs', transport : 'stdio' }, casper: { clientScripts: [ "uniqueDom.js" ], logLevel : 'info', verbose : true, waitTimeout : 3000 }} , function(err){ spooky.start(); spooky.thenOpen("http://www.kyoiku.metro.tokyo.jp/consulting/procedure_and_form/format.html"); spooky.then(function(){ var uniqueDoms = this.evaluate(function(){ // PDFダウンロードボタン一覧の取得 var links = document.querySelectorAll(".icon_pdf"); // 一括ダウンロード押下処理 var res = []; for(var i=0; i<links.length; i++){ res.push($$uniqueDom.encode(links[i])); } return res; }); this.emit('message' , JSON.stringify(uniqueDoms,null," ")); this.eachThen(uniqueDoms , function(req){ this.evaluate(function(str){ var elm = new $$uniqueDom.decode(str); elm.click(); },{str : req.data}); }); }); spooky.run(); spooky.then(function(){this.exit();}); }); spooky.on('message', function (res){ console.log("{'message' : '"+res+"'}"); }); // データ受信処理 spooky.on('resource.requested', function(requestData, networkRequest) { if(!requestData.url.match(/\.pdf$/)){return;} console.log("+download : " + requestData.url); // url var filename = (function(url){ var sp = url.split("/"); return sp[sp.length -1]; })(requestData.url); request.get({ url : requestData.url, encoding : null, rejectUnauthorized: false, saveFile : filename }, function (error, response, body){ if(!error && response.statusCode === 200){ fs.writeFileSync(response.request.saveFile , body, 'binary'); } else{ console.log("responseCode : "+error); } }); }); $ node scraping.js -- {'message' : '[ "second.div[1].table[0].tbody[0].tr[1].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[2].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[3].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[4].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[4].td[1].a[1]", "second.div[1].table[0].tbody[0].tr[5].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[5].td[1].a[1]", "second.div[1].table[0].tbody[0].tr[5].td[1].a[2]", "second.div[1].table[0].tbody[0].tr[6].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[7].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[8].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[10].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[10].td[1].a[1]", "second.div[1].table[0].tbody[0].tr[10].td[1].a[2]", "second.div[1].table[0].tbody[0].tr[10].td[1].a[3]", "second.div[1].table[0].tbody[0].tr[11].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[12].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[12].td[1].a[1]", "second.div[1].table[0].tbody[0].tr[13].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[15].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[15].td[1].a[1]", "second.div[1].table[0].tbody[0].tr[16].td[1].a[0]", "second.div[1].table[0].tbody[0].tr[16].td[1].a[1]" ]'} +download : http://www.kyoiku.metro.tokyo.jp/consulting/files/demander/kaijiseikyu.pdf +download : http://www.kyoiku.metro.tokyo.jp/consulting/files/construction_breakdown/request_form.pdf +download : http://www.kyoiku.metro.tokyo.jp/consulting/files/personal_information/kojinseikyu.pdf +download : http://www.kyoiku.metro.tokyo.jp/consulting/files/correction_and_injunction/kojinteisei.pdf +download : http://www.kyoiku.metro.tokyo.jp/consulting/files/correction_and_injunction/kojinteishi.pdf +download : http://www.kyoiku.metro.tokyo.jp/consulting/files/specific_personal_information/tokuteikojinkaiji.pdf +download : http://www.kyoiku.metro.tokyo.jp/consulting/files/specific_personal_information/tokuteikojinteisei.pdf +download : http://www.kyoiku.metro.tokyo.jp/consulting/files/specific_personal_information/tokuteikojinteishi.pdf +download : http://www.kyoiku.metro.tokyo.jp/consulting/procedure_and_form/files/format/zyuyoshomei.pdf +download : http://www.kyoiku.metro.tokyo.jp/consulting/procedure_and_form/files/format/h_kakikae.pdf +download : http://www.kyoiku.metro.tokyo.jp/consulting/files/certificate/besshi.pdf +download : http://www.kyoiku.metro.tokyo.jp/lifelong/cultural_property/firearms_and_swords/files/registration_01/09syoyusya.pdf +download : http://www.kyoiku.metro.tokyo.jp/lifelong/cultural_property/firearms_and_swords/files/registration_01/09_2jyusyo.pdf +download : http://www.kyoiku.metro.tokyo.jp/lifelong/cultural_property/firearms_and_swords/files/registration_01/34kasituke.pdf +download : http://www.kyoiku.metro.tokyo.jp/lifelong/cultural_property/firearms_and_swords/files/registration_01/35kasituke_syuryo.pdf +download : http://www.kyoiku.metro.tokyo.jp/lifelong/cultural_property/firearms_and_swords/files/registration_02_01/32toroku.pdf +download : http://www.kyoiku.metro.tokyo.jp/lifelong/cultural_property/firearms_and_swords/files/registration_02_02/31sai_toroku.pdf +download : http://www.kyoiku.metro.tokyo.jp/lifelong/cultural_property/firearms_and_swords/files/registration_02_02/01naiyo.pdf +download : http://www.kyoiku.metro.tokyo.jp/lifelong/cultural_property/firearms_and_swords/files/registration_03_01/kouhusinsei.pdf +download : http://www.kyoiku.metro.tokyo.jp/lifelong/cultural_property/firearms_and_swords/files/registration_04/33saikohu.pdf +download : http://www.kyoiku.metro.tokyo.jp/lifelong/cultural_property/firearms_and_swords/files/registration_04/08bositu.pdf +download : http://www.kyoiku.metro.tokyo.jp/lifelong/cultural_property/firearms_and_swords/files/registration_05/10kantei.pdf +download : http://www.kyoiku.metro.tokyo.jp/lifelong/cultural_property/firearms_and_swords/files/registration_05/13genbutukakunin.pdf   見事に、PDFファイルが大量に一括でダウンロードすることができました。

ちょこっと解説

1. uniqueDomライブラリを使ってDOM位置を文字列化する。

まず、spooky実行時のoptionに、uniqueDomライブラリのjsファイルを読み込んで起きます。 casper: { clientScripts: [ "uniqueDom.js" ], ... } これによりevaluete内でエレメントをユニーク文字列に変換できます。 var string = $$uniqueDom.encode(element); xpathみたいなものだと考えてください。 この文字列をまたエレメントに戻す時は以下の処理です。 var element = $$uniqueDom.decode(string);

2. requestとfsを使ってデータダウンロード

request.get({ url : %URL, encoding : null, rejectUnauthorized: false, saveFile : %ファイル名(path) }, function (error, response, body){ if(!error && response.statusCode === 200){ fs.writeFileSync(response.request.saveFile , body, 'binary'); } else{ console.log("responseCode : "+error); } }); URLにダイレクトアクセスでファイルダウンロードができる場合は、上記のソースでほぼダウンロードが可能になります。 パスワードなどの認証を求められる場合などは、これにさらに処理を加えないといけないのですが、今回はここまでにしておきます。

eachThenでダウンロード実行

evaluateの返り値は、配列を取得ことはできますが、element情報を取得する事ができないため、今回はuniqueDomという関数を使って文字列一覧を取得しましたが、それを今度は"eachThen"を使って配列全てを別々のthen処理で復元します。 コマンドを実行すると、elementのユニーク文字と、ダウンロードするURLをダウンプしておいたので、内容が確認できるかと思います。

かなり便利なRPA

ダウンロードを手作業で行なって仕事をしている人などは、こうしたスクリプトを作っておくと、毎日自動的にファイルをダウンロードしてくれる簡単ツールを作る事が可能になります。 RPA(Robotic Process Automation)として最近業務系で流行りのやり方らしいので、興味のある方はサンプルソースを元に独自ソースを作ってみてはいかがですか?

人気の投稿

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。

ブログ アーカイブ