夜、戦車リストが欲しくなったときにjQueryっぽくスクレイピングしたい
あるページに載ったデータが必要なんだけど…みたいな話をしていて、なんか色々面倒だし、そうもジェイクエリーでスクレイピングするのが一番簡単便利というオチになった。
CSSセレクタ最高です。(いままでに一番HTMLのセレクタを書いたのがjQueryだからでは?)
jQueryならChainでeachつないでかかったものをグルグル回すのもお手の物ですしね!
では、PHPでは?
PHPでHTTP GETつったらfile_get_contents、これ鉄板。そ
んでもって、HTMLからDOMにするの?DomDocumentだ!両方とも最初からはいってるし最高!!
こうしてああしてああしてこうして「ドーン!!」
複雑面倒、Goutteつかいましょう。
Goutte
最近はスクレイピングあんまりやらないので、実はあんまつかったことない。
良い機会だし、ちょっと細かいルール書いたらどうなるのかなっておもいました。
GoutteはDomのパースやら操作やらはSymfonyのDomCrawlerになっています。
https://github.com/symfony/DomCrawler
CSSセレクタはこれです。
https://github.com/symfony/CssSelector
イイデスネ!こういうヨソのライブラリつかうスタイル。もっと流行ればいい。
Symfony、ライブラリが周り最近ドンドン公開されてて、割と周辺のライブラリには組み込まれていており、最高に便利だし、最高に最高なので超応援したい。
俺もSymfony1のころはつかってたし!(どうでもいい)
さて
みんな大好きjqueryだと
$('body span.class_name').each(function(){ console.log($(this).text()); });
みたいなのありますよね、DomCrawlerだと、たとえばこんな書き方に。
$crawler->filter('body span.class_name')->each(function ($node, $i) { echo $node->text(); });
まあ、ドットがアローで多少長いけど、これはjqueryっぽいですね~~~いいですね~~。
試しに戦車
だれしも夜中に唐突に国と戦車の名前一覧がすっげーほしくなることがありますけど、
http://ja.wikipedia.org/wiki/%E6%88%A6%E8%BB%8A%E4%B8%80%E8%A6%A7
を適当にスクレイピングするのつくってみたらこうなった。
<?php require "vendor/autoload.php"; $g = new \Goutte\Client; $c = $g->request("GET", "http://ja.wikipedia.org/wiki/%E6%88%A6%E8%BB%8A%E4%B8%80%E8%A6%A7"); $data = $c->filter('h2')->each(function($node){ $head = $node->filter("span.mw-headline"); if(!count($head)) return; if($head->text() == '関連項目') return; $list = $node->nextAll()->filter("li a"); if(!count($list)) return; $title_list = $list->each(function($node){ return $node->text(); }); return [ 'country_name'=>$head->text(), 'tank_list'=>$title_list ]; }); $data = array_filter($data); //NULL要素を捨てる var_dump($data);
※勿論Goutteは事前にComposerでいれといてくださいね
# 出力例 array(38) { [1] => array(2) { 'country_name' => string(12) "アメリカ" 'tank_list' => array(272) { [0] => string(39) "マーモン・ハリントン軽戦車" [1] => string(11) "M1戦闘車"
いやー、めっちゃ便利だわ〜〜〜。
ところで
要素がとれなくても処理しようとすると例外があがるの、正しいんだけどjquery的な使い方でかんがえると結構だるいですね、まあtryでガッっと囲って握りつぶしてもいいですが。(追記しました)
あと、nextAll、なにがAllなのかわからないけど、nextですね、なにがAllなの?
勿論attr('プロパティ名')とかもいけますよ!hrefでリンク先がとれる!*1
まだなじめない点
WebClawlerはSplObjectStorageのインプリなので、foreachとかで回せるんだけど、単に配列のつもりでさわるとキエエってなりますね。
実際配列じゃないし、しかたないんだけど、PHPerは配列信者感あるのでめんどい。
後ねーこれが一番なんだけどねースコープの問題がだるいですね。
コード見るとわかるんですが、要はreturnした要素がリストになって帰ってきます。
それはいいとして、returnにfalseでもnullでもはいっちゃうんですよね、なのでarray_filterでnull要素捨ててるんですよね。
単純にfunction() use ($data){とかでリファレンスわたせないかなっておもったけど、まあ呼び出し元がDomCrawlerの中だし、ダメっぽいね。まさかのクロージャ生成か…?(面倒)(俺がアホなだけでした、追記みてください)
まあ、極限までjQuery!ってなら、PhantomJSつかえばいいじゃんって話なのですが。
こちらからは以上です。
2014/01/18 追記
use(&$hoge)普通につかえた、単に参照渡しするのわすれてただけやった…。
<?php require "vendor/autoload.php"; $g = new \Goutte\Client; $c = $g->request("GET", "http://ja.wikipedia.org/wiki/%E6%88%A6%E8%BB%8A%E4%B8%80%E8%A6%A7"); $tank_list = []; $c->filter('h2')->each(function($node) use (&$tank_list){ try{ $head_text = $node->filter("span.mw-headline")->text(); if($head_text == '関連項目') return; $node->nextAll()->filter("li a")->each( function($node) use ($head_text, &$tank_list){ $tank_list[$head_text][] = $node->text(); } ); }catch(\Exception $e){} }); var_dump($tank_list);
Exceptionでガッと握りつぶす方法でかいてみたら、大分すっきりしましたね。