PHP の代表的な Web スクレイピング ライブラリ Goutte をだいぶ使ったので役に立ちそうなことのメモです。

インストール
以前の Goutte は phar で提供されていましたが、最近になって Composer によるインストールになったみたい。ついでなので Composer のインストールから、コマンドのみ例示します。
Composer インストール
# cd /usr/local/bin # curl -sS https://getcomposer.org/installer | php # mv composer.phar composer
Goutte インストール
# cd path/to/goutte # composer require fabpot/goutte
Goutte の使い方
大まかな流れは以下の通り。とても簡単です。
- Goutte の読み込み
- Goutte クライアントを作成
- ページをリクエスト
- 取得した Crawler オブジェクトのメソッドでスクレイピング
// Goutte の読み込み
require_once 'path/to/goutte/vendor/autoload.php';
use Goutte\Client;
// Goutte クライアントを作成
$client = new Client();
// ページをリクエスト
$crawler = $client->request('GET', 'http://example.com/');
// ページのタイトルをスクレイピング
$page_title = $crawler->filter('head > title')->text();
注意点
日本語.jp などのマルチバイト ドメイン名(IDN)を含む URL は、そのままでは GET(名前解決)に失敗します。事前に Punycode(ピュニコード)へのエンコードが必要です。ついでなので、よさげなエンコード / デコード ライブラリもご紹介。
- Punycode – Wikipedia
- GitHub – true/php-punycode: A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA) for PHP
クライアントと HTTP リクエスト
Goutte のクライアント モジュールは、Symfony の BrowserKit\Client をベースに、Guzzle HTTP Client ライブラリを組み合わせて構成されています。
Goutte クライアントの設定について、いくつか例を挙げます。
ユーザーエージェント(UA)の設定
デフォルトの「Symfony2 BrowserKit」から「My Crawler」に変更する例は以下です。
$client->setHeader('User-Agent', 'My Crawler');
Max Redirects の設定
リクエストしたページが301や302のリダイレクトを返してきたときに何回までそのリダイレクト先を辿るかを設定できます。デフォルトではリダイレクトを辿りません。
$client->setMaxRedirects(1);
尚、最後に辿り着いたURLは以下で取得できます。
$client->getRequest()->getUri();
また、リクエスト履歴として Symfony BrowserKit のヒストリー オブジェクトを参照できます。
スクレイピング
いよいよ本題。Goutte の HTML パーサー部分は Symfony の DomCrawler コンポーネントです。クライアント リクエストの戻り値として取得した Crawler オブジェクトを操作してページをスクレイピングできます。詳細は Symfony のドキュメントを参照。
- The DomCrawler Component (The Symfony Components)
- DomCrawlerコンポーネント | Symfony2日本語ドキュメント(英語版より少し古いです)
以下にいくつか使用例を記載します。
DOM 要素のテキストを取得
CSS セレクタ形式の filter() メソッドと、XPath 形式の filterXPath() メソッドが使えます。尚、filterXPath() は、以下のように「descendant-or-self::」から始めた方が無難なようです。
// filterXPath() の例
$entry_title = $crawler->filterXPath('descendant-or-self::body/div[1]/h1');
// filter() の例
$entry_title = $crawler->filter('h1.entry-title')->text();
DOM 要素の属性を取得
例えば、取得したページ内のすべてのアンカー(a)要素の href 属性は attr メソッドと each メソッドを使って以下のように取得できます。
$links = $crawler->filter('a')->each(function($node) {
return $node->attr('href');
});
$links は href 属性値の配列になります。
特定の DOM 要素を除外
reduce() というメソッドがあります。無名関数の戻り値として FALSE を返すと、そのノードが除外されます。
// 偶数番目の li 要素のみを取得
$li_even = $crawler->filter('ul li')->reduce(function($node, $i) {
return ($i % 2) == 0;
});
Goutte を HTML パーサーとして使う
Crawler オブジェクトを単体で使って任意の HTML 文字列をパースできます。
use Symfony\Component\DomCrawler\Crawler;
$crawler = new Crawler();
$crawler->addHtmlContent('<a href="/link/to/page/"><img alt="example" src="image.example.com/image.jpg" /></a>');
$img_src = $crawler->filter('img')->attr('src'); // image.example.com/image.jpg
Goutte で POST や HEAD リクエスト
せっかくGoutteを導入したなら、POST リクエストは file_get_contents() より Goutte の方が簡単です。ステータス コードだけ取得したい(200 なのか 404 なのかだけ知りたい)場合は、ヘッダ情報のみをリクエストする HEAD を使うと帯域に優しくてオススメ。
// POST
$client->request('POST', 'http://example.com/form/', ['name' => 'Hello', 'message' => 'World']);
// HEAD
$client->request('HEAD', 'http://example.com/health-check/');
$status_code = $client->getInternalResponse()->getStatus();
リクエスト部分は Guzzle です。PUT や DELETE なども可能。
HTTP レスポンス
Goutte クライアントはデフォルトで Crawler オブジェクトを返しますが、JSON が返される API を GET する場合など、必要に応じて Symfony BrowserKit のレスポンス オブジェクトを参照することもできます。いくつか使用例を記載します。
$client->request('GET', 'api.example.com/data/?ID=100');
// レスポンス ボディの取得
$json = json_decode($client->getInternalResponse()->getContent());
// ステータス コードの取得
$status = $client->getInternalResponse()->getStatus();
// レスポンス ヘッダの取得
$response_headers = $client->getInternalResponse()->getHeaders();
Goutte は XML もパースしてスクレイピングできる
以前の記事で「XMLがパースできない」と書きましたが、できました。
フィルターするときに名前空間の指定が必要なようです。デフォルトの名前空間は「default」なので、例えばサイトマップのロケーション(url)一覧を取得するなら以下になります。
$sitemap = $client->request('GET', 'http://example.com/sitemap.xml');
$urls = $sitemap->filter('default|loc')->each(function($node) {
return $node->text();
});
その他、詳しくは先ほどリンクした Symfony の公式ドキュメントをご覧ください。
The default namespace is removed when loading the content if it’s the onlynamespace in the document. It’s done to simplify the xpath queries.
などの記載に罠の香りを感じます。
リクエストやスクレイピングのエラー処理
ページをリクエストする($client->request)ときやスクレイピングする(filter, filterXPath)ときなどにエラーが発生した場合、各コンポーネントから例外(exception)がスローされます。try ~ catch で適宜、処理する必要があるでしょう。
$client = new Client();
try {
$crawler = $client->request('GET', 'http://example.com/');
} catch (Exception $ex) {
error_log(__METHOD__.' Exception was encountered: '.get_class($ex).' '.$ex->getMessage());
}
...
try {
$links = $crawler->filter('a')->each(function($node) {
return $node->attr('href');
});
} catch (Exception $ex) {
error_log(__METHOD__.' Exception was encountered: '.get_class($ex).' '.$ex->getMessage());
}
All You Need Is Goutte
これで Goutte はあなたのものだ。
Pingback: Goutte(PHP)でスクレイピングするときのメモ | Design Hack and Slash