Goutte(PHP)でスクレイピングしてみる

  • 48
    いいね
  • 0
    コメント

少し前、phpQueryというライブラリを利用してみたが、使い勝手は良かったがComposer対応ができてないので、Composer対応バッチリのGoutte(ゲットと発音するらしい)てのを利用してみました。特にテーブルの処理に重きを置いています。

結論

住めば都なのしょうが、Goutteは、ちょっと「クセ」がありました。

クセというか、他のライブラリとの一番の違いは「無い要素を抽出しようとするとエラーになる」ということです。ですので、要素があるかないかを意識してコードを書く必要があります(他のライブラリは基本、無い場合は無視という振る舞いのため、困惑します)。

他のサイト等でも、エラー処理が必要だ!というような記述は見かけましたが、こういうものとは思いませんでしたね。

インストール

利用したいディレクトリで、

composer require fabpot/goutte

とします。vendor/autoload.phpをrequire_onceで読み込みます。

使用例(クセの例)

下記のようなHTMLがあるとします。

サンプルHTML

2つのテーブルとYahooへのリンクがあります。ポイントは、テーブル<tr>の中身です。
1個目のテーブルの1行目はいわゆるヘッダー行でth要素のみからなり、td要素が含まれていません。これを覚えておいて下さい。

<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<title>test page</title>
</head>
<body>
<table border=1 class="table hoge" id="table1">
    <tr>
        <th>no</th>
        <th>name</th>
        <th>emai</th>
    <tr>
    <tr>
        <th>1</th>
        <td>aaa</td>
        <td>aaa@test.local</td>
    </tr>
    <tr>
        <th>2</th>
        <td>bbb</td>
        <td>bbb@test.local</td>
    </tr>
</table>
<br>
<br>
<table border=1 class="table foo" id="table2">
    <tr>
        <td>ccc</td>
        <td>ccc info</td>
    </tr>
    <tr>
        <td>ddd</td>
        <td>ddd info</td>
    </tr>
</table>
<br>
<br>
<a href="http://www.yahoo.co.jp">Yahooに移動</a>
<br>
<img src="https://avatars.githubusercontent.com/u/3616214?v=2">
</body>

ニーズとアプローチ

ここでのスクレイピングのニーズとしては、1個目のテーブルの各要素を取得したいものとします。
取得に際しての疑問としては、

  • 1個目のテーブルをどう取得するか?
  • td各要素にどうアプローチするか?

の2つが大きなポイントかと思います(クセは後者の際に総合しました)。

全テーブルのtr要素を取得

まず、テーブルを意識せず、tr要素を抜きたい!と思うと、下記のようになります。

<?php

    //ライブラリロード
    require_once './vendor/autoload.php';

    //use
    use Goutte\Client;

    //インスタンス生成
    $client = new Client();

    //取得とDOM構築
    $crawler = $client->request('GET','http://localhost/test/test.html');

    //要素の取得
    $tr = $crawler->filter('table tr')->each(function($element){
        echo $element->text()."\n";
    });

複数ある要素を取得したい場合は、->each()とすることにより、要素分ループできます。

特定のテーブルの要素のみを取得する

上記の例では、違う構造のテーブルの情報が取得されてしますので、実務上は実用性がありません。
実務的には、テーブルを特定して取得する必要があります。

テーブルを特定する方法はいくつかあります。
大きくは、

  • ID
  • CLASS
  • INDEX(タグの登場順位)

の3つになります。

ID

IDが振られていれば、まあ、わけはありません。
指定方法もCSS等でお馴染みなので、今更どうこうというものではありません。

    //tr要素の取得
    $tr = $crawler->filter('table#table1' tr)->each(function($element){
        echo $element->text()."\n";
    });

CLASS

クラスの場合は、重複が前提となります。複数のクラス名で特定できる場合は、.で繋げることで特定できる場合もあります。

    //tr要素の取得
    $tr = $crawler->filter('table.table.hoge tr')->each(function($element){
        echo $element->text()."\n";
    });

INDEX(タグの登場順位)

タグの登場順位を目視で確認し、番号で指定することもできます。

    //tr要素の取得
    $tr = $crawler->filter('table')->eq(0)->filter('tr')->each(function($element){
        echo $element->text()."\n";
    });

td要素の取得(クセ)

td要素を取得したい場合、普通に考えれば、tableのtdをeachで回せばいいのかな?と思い、下記のように書いてみました。

    //tr要素の取得
    $tr = $crawler->filter('table')->eq(0)->filter('tr')->each(function($element){

        echo $element->filter('td')->eq(1)->text()."\n";

    });

が、この場合エラーが出ます。

Fatal error: Uncaught exception 'InvalidArgumentException' with message 'The current node list is empty.' in /Applications/MAMP/htdocs/test/vendor/symfony/dom-crawler/Crawler.php:550

要は要素がないぞ!と怒られているようです。しばらくはまってたのですが、どうやら下記のように書けばOKのようです。

    //tr要素の取得
    $tr = $crawler->filter('table')->eq(0)->filter('tr')->each(function($element){

        //td要素があるときのみ取得処理を行う
        if(count($element->filter('td'))){
            echo $element->filter('td')->eq(1)->text()."\n";
        }

    });

そもそも、$crawler直下では、

$tr = $crawler->filter('table')->eq(5)->filter('tr')->each(function($element){});

というように、無い要素(テーブル)を照会しても何のエラーも出ません。。。慣れの問題でしょうが、一貫性が無いようにも感じられますね。

細かくドキュメントや仕様を呼んでいませんが、今のところ、2階層以下のfilter処理では、要素の有無を気にしないといけないのかな?というところです(どなたか明確なルールを知ってる人おしえて下さい)。

その他の要素

アトリビュート

アトリビュートは下記のように取得できます。

aのhref

    $tr = $crawler->filter('a')->each(function($element){

        echo $element->attr('href');

    });

imgのsrc

    $tr = $crawler->filter('img')->each(function($element){

        echo $element->attr('src');

    });

最後に

始めての利用で、クセのある書き方を要求されたので少々戸惑いましたが、全体としてはバランスの良いライブラリかなとも思います。