肉とビールとパンケーキ by @sotarok

少し大人になった「肉とご飯と甘いもの」

PHP+SimpleXMLElementでTwitterのスクレイピング

via. PHPでTwitterのBotを作ってみる - yuyarinの日記

取得したHTMLから目的の情報だけを取り出す。取り出したい情報は

  • ステータス番号 ($status_number)
  • ユーザ名 ($username)
  • メッセージ ($word)
  • @先 ($at)

の4つ。
うまいやり方が分からなかったので、strpos()とsubstr()で目的の情報が含まれる部分を愚直に取り出した後、preg_match()で正規表現マッチングして情報を抜き出す。これをwhileループで回す

PHPでTwitterのBotを作ってみる - yuyarinの日記

そういえば,スクレイピングってちゃんとしたことないなーと思ったのと,DOM::loadHTML - 「PHPで街を育てる」の続きの続きの続き - Do You PHP はてなを思い出して,PHP5なら素のPHPXPathとか使えるのかとか思い,せっかくだからXPathの練習がてらTwitterスクレイピングのクラス書いてみた.

最初はボットまで実装しようとしたけど,だったらServices_Twitter使っちゃえばいいじゃんとか考えて,でもAPI使うことの何がいやかって,制限にひっかかる*1のが嫌で,そもそもAPI使うならスクレイピングする必要ないじゃんとかうだうだ色々考えてしまって.
なので今回は,API使わないで取得できる,public_timeline と userのtimelineと,user with friends の3つのみ取得可能.


というかPHP5専用コード書いたのがはじめてで,例外とアクセス修飾子の使い方が正しいのかはわからないw*2 ついでにfactoryパターンもちゃんとわかってないので,なんか変なとこあったら教えてください>< *3

使い方

流れ

コード
<?php

require_once 'Twitter_Scrape.php';


try {
    $Twitter =& Twitter_Scrape::factory(TW_FRIENDS, 'sotarok');
    $body = $Twitter->getTimeline();
    $timeline = $Twitter->parse($body);
    
    var_dump($timeline);
    
} catch (Exception $e) {
    echo $e->getMessage() . "\n";
}
結果
[sotaro@centos php]$ php twitter_bot.php
array(20) {
  [0]=>
  array(5) {
    ["id"]=>
    string(9) "622329362"
    ["name"]=>
    string(8) "shimooka"
    ["body"]=>
    string(73) "[B!]m-takagiの実験室(12:41) http://www.m-takagi.org/ 神のサイト."
    ["permalink"]=>
    string(46) "http://twitter.com/shimooka/statuses/622329362"
    ["date"]=>
    string(25) "2008-01-21T03:43:52+00:00"
  }
  [1]=>
  array(5) {
    ["id"]=>
    string(9) "622328542"
    ["name"]=>
    string(8) "ukstudio"
    ["body"]=>
    string(38) "@forestk Gの人、はじめてみた!"
    ["permalink"]=>
    string(46) "http://twitter.com/ukstudio/statuses/622328542"
    ["date"]=>
    string(25) "2008-01-21T03:43:33+00:00"
  }
  [2]=>
  array(5) {
    ["id"]=>
    string(9) "622327832"
    ["name"]=>
    string(5) "rhaco"
    ["body"]=>
    string(76) "greeでアバタ使いたいなー、でも携帯で使う気はないなー"
    ["permalink"]=>
    string(43) "http://twitter.com/rhaco/statuses/622327832"
    ["date"]=>
    string(25) "2008-01-21T03:43:13+00:00"
  }
  [3]=>
...

コード Twitter_Scrape.php

流れは,

  • HTTP_Requesetで取得
  • XPathで解析
    • まぁココがキモなんだけど.とにかく正規表現を一度も書かずに要素を取得できたのはデカい.これだと元のHTMLの構造が変わっても対応しやすいよね.


parseでかえってくる値は,

  • id (ステータスのID)
  • name : 発言者のユーザ名
  • body : 本文 (URLも@もそのまま含む)
  • permalink : URL
  • date : 発言時間(UTC文字列)


body の内容の取得の部分ですが,replyがあったりURLが含まれててaタグがあると,子ノードが発生しちゃってそのままだとうまくとれなかったので,asXMLで取得してタグ除去してるんだけど,ここがあんま綺麗じゃないなーと.チャイルドまで全部テキストで取得する方法はないものでしょうか.なんかありそうですが知らないので適当(ぁ

実行環境は

  • PHP 5.2.3
  • required PEAR::HTTP_Request
<?php

require_once 'HTTP/Request.php';

define('TW_PUBLIC'  , 100);
define('TW_USER'    , 101);
define('TW_FRIENDS' , 102);


/**
 *  Twitter_Scrape
 *  
 *  @author     Sotaro KARASAWA <sotaro.k /at/ gmail.com>
 *  @version    0.0.1
 *  @access     public
 */
class Twitter_Scrape
{
    protected $username = "";
    
    protected $timeline;
    
    public $contents;
    
    public function __construct()
    {
    }
    
    public static function &factory ($timeline, $username = null)
    {
        $c = new Twitter_Scrape();
        
        $c->timeline = $timeline;
        
        if ($c->timeline != TW_PUBLIC) {
            if ($username === null) {
                throw new Exception('Exception : USERNAME is required');
            }
        }
        
        $c->username = $username;
        
        return $c;
    }
    
    public function getTimeline ()
    {
        switch ($this->timeline)
        {
            case TW_PUBLIC:
                $url = 'public_timeliness';
                break;
            case TW_USER:
                $url = $this->username;
                break;
            case TW_FRIENDS:
                $url = $this->username . '/with_friends';
                break;
            default:
                return false;
        }
        
        return $this->_getTimeline('http://twitter.com/'. $url);
    }
    
    protected function _getTimeline($url)
    {
        $Request = new HTTP_Request($url);
        if(PEAR::isError($Request->sendRequest())) {
            throw new Exception ('Exception : false to get timeline');
        }
        
        $body = $Request->getResponseBody();
        
        return $body;
    }
    
    /**
     *  parse
     *
     *  @param  string  $body       HTML
     *  @return array   $contents   
     */
    public function parse ($body)
    {
        // TwitterのHTMLのせいで,@ つけないとwarning出まくり
        $DOM = @DOMDocument::loadHTML($body);
        $XML = simplexml_import_dom($DOM);
        
        $arr = $XML->xpath('//tr[@class="hentry"]');
        
        $i = 0;
        foreach ($arr as $val) {
            
            $content = $val->xpath('td[@class="content"]');
            
            // application と repliesも取得しようと思ってたんだけど,途中でめんどいことに気づいてやめた
            $this->contents[$i++] = array(
                'id'        => substr((string)$val['id'], 7),
                'name'      => (string)$content[0]->strong->a,
                'body'      => trim(strip_tags($content[0]->span[0]->asXML())),
                'permalink' => (string)$content[0]->span[1]->a[0]['href'],
                'date'      => (string)$content[0]->span[1]->a[0]->abbr['title'],
                //'application'  => (isset($content[0]->span[1]->a[1])) ? (string)$content[0]->span[1]->a[1] : null ,
                //'replies'   => (isset($content[0]->span[1]->a[2])) ? substr((string)$content[0]->span[1]->a[2], 12)  : null,
            );
        }
        
        return $this->contents;
    }
}

で,

最後の最後でSimpleXMLElementのおいしさがわかったので,それは今度まとめます.
すべての子ノードにオブジェクトとしてアクセスできる点がすごく楽な点だと思います.たぶんtoStringでノードテキストが取得できるのね.だからStringキャストが必要になるというわけです.

修正した

  • タイトルちゃんとした.
  • constractのはぢい間違い修正

*1:TwitterIrcGateway使ってるから,常に制限いっぱいカツカツなはず

*2:しかもトップレベルのExceptionはcatchしないほうがいいの?

*3:デザパタ本買ったのにまだ読んでいないという