PHPでスクレイピング

例えばふぁぼるっくというサイトから、各ふぁぼの内容を'ツイート'・'個別ページへのリンク'・'ふぁぼ数'を取得する場合、私だったらこうします。

```php
<?php
use Diggin\Scraper\Scraper;

$scraper = new Scraper;
$scraper->process(
    '#postlist li', array('postlist[]' => (new Scraper)
        ->process('//div[@itemprop="articleBody"]', 'articleBody', 'html')
        ->process('//div[@class="name"]/a', 'author_page', '@href', function($uri) { return $uri->toString();})
        ->process('//ul["favlist"]//p[@class="fav"]', 'fav', 'raw', function($v) {return (int) $v;})
    )
);
$ret = $scraper->scrape('http://favlook.osa-p.net/index.html');
var_dump($ret['postlist']);
```

実行結果は、
<pre style="background:#ccc;">
array(30) {
  [0] =>
  array(3) {
    'articleBody' =>
    string(51) "３個めに手が伸びた瞬間、蘇る記憶。"
    'author_page' =>
    string(68) "http://favlook.osa-p.net:80/status.html?status_id=337187558524063745"
    'fav' =>
    int(4)
  } 
  [6] =>
  array(3) {
    'articleBody' =>
</pre>

という戻り値です。

さて、この投稿では
・スクレイピングでは何を留意すれば良いか
・Digginの各コンポーネントがどのように処理をおこなっているか
・PHPスクレイピング関係の事情
を記します。

## スクレイピングでは何を留意すれば良いか
→PHPでWEBアプリケーションを作るのと同じこと

あなたが作るWebサイトはきっと以下のようなページです
・リクエストに基づきレスポンスを返す
・レスポンスにはContent-Typeなどのヘッダーをつける
・gzipなどで圧縮する
・認証を元にレスポンスを返す
・XML validに遠かったり・近かったりするHTML
・ブラウザに判断できればOKなのでリンクは相対パス
・jsを用いたより動的なページ
などなど

上記のことをレスポンスを返却する側ではなく、リクエストを送って処理する側で考えれば良いのです。。。。
そうですね、
# 面倒くさい！！！！！！！！！！！！
ということで上記のコードで用いた PHPスパイダリングコンポネーント Diggin
では内部的にこの面倒くささにどう対応しているか解説します。


## Digginの処理の内側
上記のコードでは以下のような処理を内部的に行っています。

### リクエスト(Zend\Http\Client)
    file_get_contents使いません。なぜ？
    scrape()メソッドに渡したURLに対し、
    Zend\Http\Clientにてリクエストを送信しています。
    Zend\Http\Clientをここで用いているfile_get_contentsに対するメリットは、
    ・Basic認証/Cookie処理
    ・gzipの場合は自動デコード
    ・Testアダプタでのデバッグ時のMock化
    ・レスポンスのオブジェクト化
    ・(デフォルトで)ソケットクライアント
　　・リダイレクト処理
    などがあります。 

### 文字コードの自動判定、UTF-8への変換 (Diggin_Http_Charset)
    毎回文字コードを目視してmb_convert_encodingなんて面倒です
    
    ・文字コード判定その１
     レスポンスヘッダーとメタタグによる
      （もちろんShift-JISというContent-Typeは一切信頼せず、
        SJIS-winかなどを判定優先します）
    ・文字コード判定その２　その１の処理に加えmb_detect_encoding
    ・UTF-8 BOMの除去
    ・mbstringのサポート外の文字コードの場合はiconv利用

### HTMLの整形 (Diggin_Scraper_Adapter_Htmlscraping)
    あなたがブッコ抜きたい情報があるサイトほどHTMLソースは汚いものです
    Htmlscrapingクラスは、Http_Requestと密で
    レスポンスオブジェクトを個別に切り離して使用できなかったため、
    処理を流用しています。
    ・tidyによる自動整形
    ・不要なタグの削除
    ・SimpleXMLElement->asXML()でのダンプに対するエンティティ参照に対するラップ


### DSLライクな記法 (Diggin\Scraper)
    スクレイピングのコードから意図を明確にし、重複を減らしましょう
    Diggin_ScraperではperlのWeb::Scraperを参考に
    process('xpath、cssセレクタ', 'キー', 処理形式, フィルタ)
    という形でスクレイピングの命令を集約します
 
### 多次元配列での取得
    上記のコードにあるとおり、Scraperオブジェクトに対し、
    別のプロセスを持ったScraperオブジェクトをセット
    することにより多次元配列として取得できます。
    もしかして、foreachで取得した文字列を変換したい？
    あらかじめprocess()メソッドにてフィルタを指定しておけば必要はありません。

### URLの自動変換
    '@href'や'@img'でプロセス指定があったら、
    もしHTML中に相対パスで記述されていても、
    フルパスのURLに自動変換されたものを取得します。

Digginではさらに、「次ページ」リンクの処理を自動化するべく、Diggin_Service_Wedataを用意し、ScraperオブジェクトからPagerizeヘルパーとして利用ができるようにもしています。

さて、
> この投稿は使える/正しい情報だと思いますか？

え、違った？他にも何かやり方があると聞いてる？昨今のスクレインピングまわりでの事情を次節で紹介します

## PHPスクレイピングまわりの昨今
一昔前ならば、
「PHPでのスクレイピングに役立つライブラリ」(http://dxd8.com/archives/85/)
にある程度まとまってましたが、古めかしいです。

ここ２、3年で話題になったものと言えば、
・simple_html_dom
・Goutte
・htmlSQL
・phpQuery
とかでしょうか。そのうち、htmlSQLについてはHTMLというドキュメントツリーに対してSQLというデータ構造に対するアプローチへの偏頭痛の上に、作者が別のすすめてるのに紹介する人はなんなのかよくわかりません。simple_html_domはPHP本体で提供されているDOMとは別にゴリゴリ処理を書かれてる点で面白いのですが、xpath使いたいですし、メモリを使い果たすという噂も聞くので避けてます。Goutteについては、http://d.hatena.ne.jp/hnw/20120115 に詳細がかかれてますが、ラッパー・上位互換的な存在のBehat\Minkにも注意払うべきでしょう。ってスクレイピングというよりスパイダリング領域の話ですが。

昨今のスクレイピングまわりでの動向としては以下のものがあります。
・HTTPクライアント (Guzzle,requests)
・BDD (Behat/Mink)
・ヘッドレスブラウザー(WebKitGTK/PhantomJS/CasperJS)
・jQueryライクを唄うDOMライブラリ(ググって)
上記の詳細は割愛しますが、最後に、Doctrineリードデベロッパーが作った、
https://github.com/beberlei/phpricot
を眺めながらpecl/html_parseとはなんだったのか感慨にふけりながらこの記事を終わります。


## 参照リンク
・PHPでスクレイピング http://qiita.com/items/5f26263d9ebf4268d7f3
・Diggin https://github.com/diggin
・Digginリファレンス http://diggin.musicrider.com/manual/diggin.index.html
・Behat\Mink https://github.com/Behat/Mink
・xpathで正規表現を使う (registerXpathPhpFunctionsの使い方)
http://qiita.com/items/179957d5aefe0e3785ab
・https://twitter.com/everzet/status/333666328521109504