FirePHPを用いた、スクレイピングデバッグに関する考案


今回は、架空のサイトevents.php.bunko.jpを対象にスクレイプ&spidering対象にします。
仮に、勉強会の参加回数を聞かれて答えに窮する状況があったとします。
参加回数の表示機能は、カリオストロ城の奥地に眠っているらしくすぐに使うのは困難なようです。
特定のメンバー名だけならすぐにカウントできそうですが、メンバー一覧を取得しDBに格納したいものです。

取得する主な対象は、メンバー一覧ですが、発表タイトルでどのように参加状況が変わるかも調べてみたいとします。

<?php
//発表タイトル
$titles = new Diggin_Scraper_Process();
$titles->process('//h4', 'title[] => TEXT');

//参加者
$members = new Diggin_Scraper_Process();
$members->process('/td[1]', "name => TEXT")
       ->process('/td[@style="text-align:center"]', "comment =>TEXT")
       ->process('/td[3]', "party => TEXT")
       ->process('/td[4]', 'timestamp => TEXT');

// /div[@id="content"]内で、どのブロックに該当するか指定
$content = new Diggin_Scraper_Process();
$content->process('/div[3]', array('titles' => $titles))
        ->process('/div/table/tr[@class="odd" or @class="even"]', array('members[]' => $members));
       
//HTML内の対象範囲を指定し、スクレイプ
$scraper = new Diggin_Scraper();
$scraper->process('//div[@id="content"]', array('event' => $content))
        ->scrape($url);

$urlがhttp://events.php.gr.jp/events/show/67
を対象としていた場合は上手くいってるようです。

ほかのeventsページを対象にした場合でも上記のxpathが通用するのか検討します。
Diggin_Scraperでは通常xpathなどの指定対象にマッチするものがない場合、
Diggin_Scraper_Strategy_Exceptionを送出します。
これを、Zend_Wildfireなどと連携させることを考慮した場合のコードは以下です。

<?php
//これをテストスクリプトと同階層のDiggin/Scraper/Strategy/Exception.phpに保存
class Diggin_Scraper_Strategy_Exception extends Exception
{

   public static $logger;

   public function __construct($msg)
   {
       self::$logger->log($msg, Zend_Log::INFO);
   }

   static public function logging($logger)
   {
       self::$logger = $logger;
   }
}
<?php
$path = get_include_path();
$s = set_include_path(dirname(__FILE__).PATH_SEPARATOR.$path);

require_once 'Zend/Loader.php';
Zend_Loader::registerautoload();

$writer = new Zend_Log_Writer_Firebug();
$logger = new Zend_Log($writer);

Diggin_Scraper_Strategy_Exception::logging($logger);

$request = new Zend_Controller_Request_Http();
$response = new Zend_Controller_Response_Http();
$channel = Zend_Wildfire_Channel_HttpHeaders::getInstance();
$channel->setRequest($request);
$channel->setResponse($response);

/**
 * cache
 */
$frontendOptions = array(
    'lifetime' => 86400,
);
$backendOptions = array(
    'cache_dir' => dirname(__FILE__).DIRECTORY_SEPARATOR.'cache'
);

$cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions);

$url = 'http://events.php.gr.jp/events/show/67';
$key = str_replace(array(':', '/', '?', '.'), '_', $url);

if(!$httpResponseString = $cache->load($key)) {
    $httpClient = new Zend_Http_Client($url);
    $httpResponse = $httpClient->request();
//    $decoResponse = new Zend_Http_Response($httpResponse->getStatus(), 
//                                  $httpResponse->getHeaders(),
//                                  $httpResponse->getRawBody(),
//                                  $httpResponse->getVersion(),
//                                  $httpResponse->getMessage());
    $cache->save($httpResponseString = $httpResponse->asString("\r\n"), $key);
}
try {
    $res = Zend_Http_Response::fromstring($httpResponseString);
} catch (Exception $e) {	
    echo $e->getMessage();exit;
}


ob_start();

$logger->log("test", Zend_Log::INFO);
/**
 * scrape
 */

try {
    //発表タイトル
    $titles = new Diggin_Scraper_Process();
    $titles->process('//h4', 'title[] => TEXT');
    
    //参加者
    $members = new Diggin_Scraper_Process();
    $members->process('/td[1]', "name => TEXT")
           ->process('/td[@style="text-align:center"]', "comment =>TEXT")
           ->process('/td[3]', "party => TEXT")
           ->process('/td[4]', 'timestamp => TEXT');

    // /div[@id="content"]内で、どのブロックに該当するか指定
    $content = new Diggin_Scraper_Process();
    $content->process('/div[3]', array('titles' => $titles))
            ->process('/div/table/tr[@class="odd" or @class="even"]', array('members[]' => $members));
           
    //HTML内の対象範囲を指定し、スクレイプ
    $scraper = new Diggin_Scraper();
    $scraper->process('//div[@id="content"]', array('event' => $content))
            ->scrape($res, $url);
} catch (Exception $e) {
    $logger->err($e);
    $channel->flush();
    $response->sendHeaders();
    die();
}
$logger->log($scraper->results, Zend_Log::INFO);
$channel->flush();
$response->sendHeaders();

//var_dump($res);
$headers = $res->getHeaders();
unset($headers['Set-cookie']);unset($headers['P3p']);
var_dump($headers, $res->getBody());

実際の画面上では以下のように表示されます。


では、他のページhttp://events.php.gr.jp/events/show/62
だとどうなるかというと、

「 Couldn't find By Xpath, Process : './/h4', 'title => "TEXT"'」とxpathでの取得に失敗した箇所があることが分かります。(なお、.//h4と先頭ドットつきなのは、simlpexmlelementイテレータxpath指定したノードをさらに指定させるため)
Diggin_Scraperでは現在、scrapeメソッドを実行した主ブロックでは、取得対象に該当するものがなかった場合はそのままStrategy_Exceptionを送出し、それ以外の副ブロックでは配列要素として格納しません。(ただ、最近まで@hrefとか属性を取得値とした場合の考慮が抜けてたりして、まだ上手くいってない箇所がありますが。。)
この取得対象の取扱いは、主ブロックがこけた場合はHTMLレイアウトが大いに変更されたのでスクレイプしない、副ブロック以降の箇所で該当範囲がないのは、ユーザー状態・ページングにより変更される箇所というオレオレ思想です。