PHPのWebスクレイピング・ライブラリ「Goutte」と「phpQuery」を比較してみた

  • 129
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

私はWordPressの自作プラグインを公式SVNに登録しているのだが、ダウンロード状況等のステータスを毎度WordPress.orgまで見に行くのが億劫になってきたので、自分のブログサイトにその情報だけスクレイピングしてきて楽したいなぁ…と思い立った。

以前、自分でスクレイピング処理を書くのはやったことがあるんだが、その時はソース元のHTMLが汚かったこともあって、非常に面倒だった記憶だけが残っている。私の脳内ではスクレイピング=鬼門なのだ(笑) まぁ、他所のサイトの、しかも一世代前のXHTMLあたりで書かれた(おそらくシステムが自動で吐き出していると思われる)構造だけが複雑なHTMLのパース処理をコーディングをしていると、モチベーションがただひたすらに下がり続けるものなんだ──という苦い経験は今回はしたくない。

そんなわけで、今回はPHPのスクレイピングツールを利用して、サクっと作ってみようと思った次第。検索してみたところ、引っかかった中では「Goutte」と「phpQuery」が一番使い易そうだった。今回の記事では、この二つのライブラリでスクレイピング処理を書いて、実際に動かして、ベンチマーク取ってみての比較をまとめてみた。

導入状況を比較

まず、それぞれのライブラリのスペックを比較してみる。

Goutte phpQuery
配布元 https://github.com/FriendsOfPHP/Goutte https://code.google.com/p/phpquery/
インストール方法 実行ファイルをインクルード 実行ファイルをインクルード
実行ファイル名 goutte.phar
(Pharアーカイブ形式)
phpQuery-onefile.php
(PHPファイル)
ファイルサイズ 720KB 164KB
最新バージョン 2.0 0.9.5
推奨PHPバージョン 5.4 以降 5.2 以降
必要な依存ライブラリ Guzzle 4.* 特になし
備考 PHP 5.3系で利用する場合はGoutte1.0.6+Guzzle3が必要 CLIもあり、コマンドラインからも利用できる

※ 2015年1月30日時点

導入のし易さとしては、特に依存関係のあるライブラリもなく、対応するPHPバージョンも幅があるので、 phpQuery の方にやや軍配が上がるかなぁ。まぁ、Goutte にしてもコマンド一発で依存系含めてインストールできるので、特に導入に手間取ることはなかっし、導入時のスペック差はあまりないという印象だ。

コーディング仕様を比較

ここでは実際にコードを見てもらうのが早いかと思うので、それぞれのライブラリで同じ処理を書いてみた。処理内容としては、WordPress.org の任意のプラグイン(テーマも可)のステータス状況のページから各種利用状況系のデータだけを取得して、再利用し易いPHPの配列に格納するという、まぁWebスクレイピングを使った、いわゆるクローラー的な処理である。

まずは、 Goutteを利用したスクレイピング処理 の例:

use_goutte.php
<?php
// Plugin Name
$extend_type = 'plugins'; // or themes
$extend_name = 'custom-database-tables';

require_once '/wp-content/themes/my_theme/lib/vendor/goutte.phar';

use Goutte\Client;

// Create Goutte Object
$client = new Client();

// Get Data Source
$crawler = $client->request('GET', "https://wordpress.org/{$extend_type}/{$extend_name}/stats/");

// Get DOM Objects
$plugin_title = $crawler->filter('div#plugin-title h2');
if ($extend_type == 'plugins') {
    $contents_path = 'div#pagebody div.wrapper div.col-10 div div.col-3 p';
} else {
    $contents_path = 'div#fyi ul';
}
$sidebar_info = $crawler->filter($contents_path);
$history = $crawler->filter('div#history');

$stats = array(ucfirst(rtrim($extend_type, 's')) . ' Name' => $plugin_title->text());

$sidebar_info->each(function ($node) use ($extend_type, &$stats) {
    $labels = array();

    $text_source = $node->text();

    $children = $node->filter('strong');
    $children->each(function ($node) use (&$labels) {
        $labels[] = $node->text();
    });

    foreach (array_reverse($labels) as $label) {
        if (preg_match("/{$label}/", $text_source)) {
            list($text_source, $value) = explode($label, $text_source);
            $label = rtrim(trim($label), ':');
            if ($extend_type == 'themes') {
                list($value, ) = preg_split('/[\s]{1,}/', trim($value));
            }
            $stats[$label] = trim($value);
        }
    }

});

$history->each(function ($node) use (&$stats) {
    $labels = array();

    $rows = $node->filter('tr');
    $rows->each(function ($node) use (&$stats) {
        $key = trim($node->filter('th')->text());
        $value = trim($node->filter('td')->text());
        $stats[$key] = $value;
    });

});

var_dump($stats);

次に、phpQueryを利用したスクレイピング処理 の例:

use_phpQuery.php
<?php
// Plugin Name
$extend_type = 'plugins'; // or themes
$extend_name = 'custom-database-tables';

require_once '/wp-content/themes/my_theme/lib/vendor/phpQuery-onefile.php';

// Get Data Source
$html = file_get_contents("https://wordpress.org/{$extend_type}/{$extend_name}/stats/");

// Get DOM Object
$dom = phpQuery::newDocument($html);

// Parse DOM Objects
$plugin_title = $dom['div#plugin-title h2'];
if ($extend_type == 'plugins') {
    $contents_path = 'div#pagebody div.wrapper div.col-10 div div.col-3 p:first';
} else {
    $contents_path = 'div#fyi ul';
}
$sidebar_info = $dom[$contents_path];
$history = $dom['div#history table'];

$stats = array(ucfirst(rtrim($extend_type, 's')) . ' Name' => $plugin_title->text());

$text_source = $sidebar_info->text();
$labels = array();
foreach ($sidebar_info->find('strong') as $label) {
    $labels[] = pq($label)->text();
}

foreach (array_reverse($labels) as $label) {
    if (preg_match("/{$label}/", $text_source)) {
        list($text_source, $value) = explode($label, $text_source);
        $label = rtrim(trim($label), ':');
        if ($extend_type == 'themes') {
            list($value, ) = preg_split('/[\s]{1,}/', trim($value));
        }
        $stats[$label] = trim($value);
    }
}

foreach ($history->find('tr') as $row) {
    $key = trim(pq($row)->find('th')->text());
    $value = trim(pq($row)->find('td')->text());
    $stats[$key] = $value;
}

var_dump($stats);

実行すると、どちらも下記のようにプラグインの利用状況をWordPress.orgから取得してきて、再利用し易いPHPの配列にスクレイピングしてくれる。

スクレイピング元となるWordPress.orgのプラグイン利用状況ページ
WordPress.orgのプラグイン利用状況ページ

スクレイピングの結果
スクレイピング結果表示

コーディングのし易さでも、phpQuery の方がコード量が少なくて済むので扱い易い印象があった。 Goutte では名前空間を利用しているので、グローバル空間側で定義した変数などを Client クラスに受け渡す場合や、 Client クラス内のメソッドへ変数を受け渡す時などに、リファレンス扱いで参照渡ししないとそれぞれの名前空間のスコープで変数が共存できなかったりしてちょいと面倒だな…と感じた次第。まぁ、この辺は好き嫌いの範疇だなぁ…。
特筆すべき点としては、スクレイピング元のURLからファイルを取得する処理の差だろうか。というのも、 Goutte では専用の request() メソッドがあって、そこではGET以外にPOSTメソッドタイプにてファイル取得ができる点が優れている。これによってSubmit後にしか表示できないようなURLにも対応できる可能性があるのだ。一方で、 phpQuery ではファイル取得はライブラリに依存しない形で別途コーディングしないといけない。例では file_get_contents() 関数で取得しているが、POSTメソッドを指定してアクセスしたい場合などは結構手間が増えるかもしれない。
取得したデータをDOMパースした後のコンテンツ捜査性については、どちらもあまり差はなく、CSSのセレクタ形式(jQueryのセレクタと同じ)のパスで要素を指定できる。そして、セレクタ形式パス指定で取得したDOMオブジェクト(PHPオブジェクト)に対してはメソッドを指定してデータ取得が行えるのも同じだ。まぁ、この部分の差があるとすれば、 phpQuery の方がJQueryのメソッド名に近しいので、jQuery知っているならこちらの方が扱い易い程度かなぁ。

パフォーマンスの比較

さて、ここでは前項で紹介した同じスクレイピング処理をパフォーマンスの点で比較してみた。まずは、処理時間からだ。

処理時間の比較

テスト内容としては、30回同じURLに対してスクレイピングリクエストを実行して、まず外部ライブラリを読み込んで初期化完了するまでの時間、次にURLリクエストの応答と取得データのDOMオブジェクトパース処理の時間、最後にコンテンツ捜査処理が完了するまでの時間、そしてトータルの全処理時間の平均値を出してみた。 Goutte のみPOSTメソッドでもスクレイピングを実行してみたので、その際の平均値は()内に記載してある。

Goutte phpQuery
ライブラリ初期化 0.001300057 (0.003100181) 0.001366655
URLリクエスト応答+
DOMオブジェクト生成
0.716607642 (0.760643625) 0.869116362
コンテンツ捜査 0.039035694 (0.039702058) 0.016300988
全処理合計 0.756943393 (0.803445864) 0.886784005

結果としては、 Goutte の方がパフォーマンスが良かった。URLリクエストで取得したデータ(html)からDOMオブジェクトを生成する処理で差が大きかった感じだ。なお、 Goutte では同一URLへのGETメソッドでのリクエストをキャッシュするようで、初回のリクエストより後は処理時間が短くなっていた(一方、POSTメソッドでは毎回同じような処理時間だった)。面白いのが、生成したDOMオブジェクトに対してのコンテンツ操作性能では phpQuery の方が優れていた点だ。捜査時間が Goutte の半分以下である。
全体の処理時間性能としては Goutte に軍配が上がりそうだが、一度URLリクエスト応答して取得済みのオブジェクトから様々な異なるコンテンツ捜査を複数回行うような処理の場合は、コンテンツ捜査性能が高い phpQuery の方が有利になるかもしれない。

使用メモリの比較

テスト内容は前述の処理時間と一緒だが、回数重ねてもほぼ近似値しか得られなかったので、それぞれ10回ずつで検証してみた。
なお、メモリの場合、プロセスに割り当てられるメモリの最大値に対して実プロセスでどれだけメモリを使用しているかを測ることで、そのアプリケーション(ライブラリ)がどの程度効率的にメモリを使っているのかとかもわかる。項目が多いのでそれぞれのライブラリでまとめてみる。

phpQuery の使用メモリ

使用メモリ量(KB) 割り当てメモリ最大値(KB) メモリ効率
ライブラリ初期化 708.8133 717.793 98.75%
URLリクエスト応答+
DOMオブジェクト生成
36.04688 41.8125 86.21%
コンテンツ捜査 10.92188 159.8391 6.83%
全処理合計 755.782 919.4445 82.20%

コンテンツ捜査にかなり余裕を持ってメモリを割り当てている。だからコンテンツ捜査性能がいいんだねぇ…。まぁ、全体的にメモリ割り当て効率に無駄がなくて、賢いライブラリという印象を受けた。この辺はさすがGoogleと言ったところかな(笑)

Goutte の使用メモリ

使用メモリ量(KB) 割り当てメモリ最大値(KB) メモリ効率
ライブラリ初期化 326.4375 369.2656 88.40%
URLリクエスト応答+
DOMオブジェクト生成
1217.562 1266.816 96.11%
コンテンツ捜査 410.6547 375.4781 109.37%
全処理合計 1954.654 2011.56 97.17%

うお!? ちょっと、コンテンツ捜査処理で使用されるメモリが割り当て値を超えちゃってますよ(笑)これではせっかくその前までのプロセスがパフォーマンス良くても、ここの処理いかんでは詰まってしまう可能性があるかもねぇ。その辺が phpQuery よりコンテンツ捜査性能が悪い原因かもしれん。全処理としてのメモリ効率はかなりイイだけに、この内部プロセスのメモリ割り当てを最適化すればかなり良いパフォーマンスになるんではないだろうか? しかし、まぁ Goutte は結構メモリ使うななぁ…。

というわけで、使用メモリを比較した結果は、圧倒的に phpQuery の勝ちですな。
Goutte でのスクレイピングは2MB程度のメモリを食うが、 phpQuery はその半分以下のメモリでサクサク動いているという感じ。

総評

なかなか甲乙付けがたい結果ではあるんだが、あえて判定するなら、スクレイピング・ライブラリとしては phpQuery が僅差で勝ちって感じかなぁ。あくまで私の主観での総評になるが、比較結果は下記のようになるかと。

  • 導入敷居の低さ: phpQuery > Goutte (依存系ライブラリがなく、素で対応するPHPバージョンが多い)
  • コーディング: phpQueryGoutte (コード量では phpQuery だが、機能面では Goutte の方がやや優秀か)
  • パフォーマンス(処理時間): phpQuery < Goutte (今回は総処理時間で Goutte に分があったが、コンテンツ捜査処理いかんでは逆転するリスクを含んでいる)
  • パフォーマンス(使用メモリ): phpQuery > Goutte (使用メモリ効率として97%の Goutte は無駄がなくて良いのだが、総メモリ使用量と各プロセスにおける割り当てメモリの分配の仕方は phpQuery の方が優秀だった)

どっちを使うかは好み次第だなぁ…。
ちなみに私は、jQuery好きなこともあるし、使用メモリ量が少なくて済む phpQuery を選ぼうかと。