uzullaがブログ

uzullaがブログです。

夜、戦車リストが欲しくなったときに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でガッと握りつぶす方法でかいてみたら、大分すっきりしましたね。

2015/06/13 追記

最近SSL通信が失敗することがあって、正しくは証明書を別途指定するといいんだけど、「単に無視する」時のやり方が色々な所の指摘がバラバラだった(cURL_OPT指定は古い?)のでメモ。

<?php
// SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
// みたいなエラーの時(Twitterとかのコンテンツとってきたらエラー

// snip
$c = new \Goutte\Client();
$c->getClient()->setDefaultOption('verify', false);

*1:どうしてもjqueryかいてる気分になり、`.`でつないでしまってエラーで3分悩む