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

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

##結論

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

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

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

##インストール

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

```bash
composer require fabpot/goutte
```

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

##使用例（クセの例）

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

###サンプルHTML

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

```html
<!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
<?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等でお馴染みなので、今更どうこうというものではありません。

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

####CLASS

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

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

####INDEX（タグの登場順位）

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

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

###td要素の取得（クセ）

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

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

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

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

```bash
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のようです。

```php
	//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直下では、

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

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

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

##その他の要素

###アトリビュート

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

####aのhref

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

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

####imgのsrc

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

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

###最後に

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