Edited at

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

More than 1 year has passed since last update.

少し前、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');

});


最後に

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