少し前、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');
});
###最後に
始めての利用で、クセのある書き方を要求されたので少々戸惑いましたが、全体としてはバランスの良いライブラリかなとも思います。