SimpleXML&Tidy@動画サイトへの道

こんにちはー。再構築といいつつ、しばらく手を休めてましたが再開しました。
さて、Webページのスクレイピングですが、SimpleXMLとTidy関数を使います

流れとしては、
- Tidy関数でHTMLを整形
- SimpleXML化
- Xpathで抽出


HTML整形のこーどは以下の通り。

    $config = array(
        'indent'     => TRUE,
        'output-xml' => TRUE,
        'wrap'       => 200);
    $html = file_get_contents("http://xxx.xxx");
    $codeList = array("Shift_JIS", "EUC-JP", "SJIS", "UTF-8", "JIS");
    $html = mb_convert_encoding($html, "UTF-8", mb_detect_encoding($html, $codeList));
    $html = html_entity_decode($html);

    //HTML整形
    $tidy = new tidy;
    $tidy->parseString($html, $config, 'utf8');
    $tidy->cleanRepair();

文字コードを指定しなければならないので、一旦UTF8に揃えちゃいましょう
tidyのコンフィグはまだ把握してないので、おまじないになっています。


その後、SimpleXML化してXpathで抽出するのです。

$xmlObj = new SimpleXMLElement($tidy);
$xmlObj->xpath('//xpath');

ここでの注目点は、SimpleXMLのコンストラクタの引数はStringですが、ここではTidyオブジェクトを放り込んでいます。
tidyオブジェクトはStringとして参照することができるためです。
次にXpathXpathはTbodyが使えません。FirefoxでXpatherを使ってXpathを取得している人は気を付けましょう。

見直し。@動画サイトへの道

こんにちは。
今まで、勢いでやってきた動画への道ですが、ここらへんでいろいろな問題が出てきました。…というか、再構築していて気づいた…

どんなことかというと、

  • 自動化(ダウンロードなど)バッチのアルゴリズムの変更に手間がかかる。
  • DBアクセス周りが冗長になってきた
  • テーブルのカラムを変更する時に影響範囲が大きい。
  • 何より(モデルとか)ソースの置き場所がカオスな感じになっている

というわけで、分解中です。

テザリングしたいなー

せっかく回線持ってるので、テザリングしたいなーと思っています。
欲をいえば冗長化したい。
ガタイはnetwalker。USBポート→netwalker無線LANへ流したい…
冗長化と言うには弱いですが、そういうところの経験も積みたい。
というわけで、Netwalkerテザリングについて調査中。
あとは回線の冗長化ってどうやるかについてですな

ヘッダで分岐@動画サイトへの道

こんにちは。ちょっと間を空けていました。
さて、前回エントリのクローラーですが、試行錯誤の結果、大体(?)完成。
3回ぐらい書きなおしましたですよ


他の実装予定部分は今のところ置いといてます。
↓実装予定
・アクセスのインターバル
・逆順クロール=ツリーの逆探索
・深さ優先のクロール


とりあえず、何でもかんでも保存するのではなく、HTTPヘッダからそれっぽいものを抽出してDLリストへ登録します。
以下はそのソース。稚拙なコードですが…
コメントが誤っていたらすみません…

class Crowler {

    const STATUS_NOACCESS = 0;
    const STATUS_ACCESSED = 1;
    const STATUS_CACHED = 2;

    const CROWL_INTERVAL = 3;

    const MAX_TASK = 5;

    const MODE_HIERARCHY = 'Hierarchy';
    const MODE_DEPTH = 'Depth';

    const PROTOCOL = 'http://';

    const CONTENT_TYPE_OCTET_STREAM = 'application/octet-stream';
    const CONTENT_TYPE_ZIP = 'application/zip';
    const CONTENT_TYPE_EXE = 'application/x-msdownload';
    const CONTENT_TYPE_WMV = 'video/x-ms-wmv';
    const CONTENT_TYPE_MPEG = 'video/mpeg';
    const CONTENT_TYPE_MPEG_AUDIO = 'audio/mpeg';
    const CONTENT_TYPE_HTML = 'text/html';

    const HTTP_RESPONSE_200_OK = '200 OK';
    const HTTP_RESPONSE_302_REDIRECT = '302 Redirect';
    const HTTP_RESPONSE_302_FOUND = '302 Found';
    const HTTP_RESPONSE_TIMEOUT_OR_FAILED = 'failed';

    const DECISION_SKIP = 0;
    const DECISION_CROWL = 1;
    const DECISION_ENTRY = 2;
    const DECISION_FAILED = 3;


    const CROWL_NO_INTERVAL = -1;
    const CROWL_INTERVAL_RANDOM = 0;


    const CLOWLED = 1;

    protected $host = '';
    protected $cacheDir = '';

    protected $curls = array();
    protected $urls = array();
    protected $crowledUrl = array();

    protected $result = array(
        'entry_cnt' => 0,
    );
    protected $nextHierarchyUrls = array();

    protected $node = array();
    protected $linkList = array();
    protected $stack = array();

    private static $instance = null;
    private function __construct() {
    }

    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function execute($url, $conf) {
        if (isset($conf['mode']) === false) {
            return false;
        }
        if (isset($conf['reverse']) === false) { //逆順フラグ
            $conf['reverse'] = false;
        }
        $mode = $conf['mode'];
        $reverse = $conf['reverse'];

        if (isset($conf['timeout']) === true) {
            ini_set('default_socket_timeout', $conf['timeout']);
        }

        switch ($mode) {
            case self::MODE_HIERARCHY: //幅優先クローリング
                $linkInfoList = array();
                $exceptList = array();
                $linkInfoList[] = array(
                    'url'    => $url,
                    'parent' => '/',
                );

                while (true) {
                    $linkInfo = array_shift($linkInfoList);
                    if (is_null($linkInfo) === true) { //クロール対象がない場合
                        break;
                    }
                    $this->crowlHierarchy($linkInfo, $linkInfoList, $exceptList);
                }
                break;
            case self::MODE_DEPTH: //深さ優先クローリング

        }
    }

    /**
     * 幅優先のクロールをする
     */
    protected function crowlHierarchy(array $linkInfo, array & $linkInfoList, array & $exceptList) {

        $url = $linkInfo['url'];
        $parent = $linkInfo['parent'];
        $node = new Node($url);
        $info = $node->getInfo();
        $action = self::decision($info, $parent); //情報から分岐
        echo 'Que:' . count($linkInfoList) . "  ";
        switch ($action) {
        	case self::DECISION_FAILED:
                $this->fail($url);
                return;
            case self::DECISION_SKIP:
                $this->crowlSkip($url);
                return;
            case self::DECISION_ENTRY:
                $this->entryForDownload($url, $parent, basename($url), $info['Content-Length']);
                break;
            case self::DECISION_CROWL:
                $this->roundAndSet($url, $node, $linkInfoList, $exceptList);
                break;
            default:
                return false;
        }
        return true;
    }

    protected function fail($url) {
        echo "Crowl Failed {$url}  \r\n";
        Log::write(system::CROWL_LOG, "Crowl Failed {$url}  \r\n");
    }
    protected function crowlSkip($url) {
        echo "Crowl Skip {$url}  \r\n";
        Log::write(system::CROWL_LOG, "Crowl Skip {$url}  \r\n");
    }
    protected function entryForDownload($url, $parent, $filename, $contentLength) {
        echo "Crowl Entry {$url}  \r\n";
        Log::write(system::CROWL_LOG, "Crowl Entry {$url}  \r\n");
        $entryInfo = Array(
            'url'      => $url,
            'parent'   => $parent,
            'filename' => $filename,
            'size'     => $contentLength,
        );
        DB_AutoDownload::entry($entryInfo);
    }
    protected function roundAndSet($url, &$node, array & $linkInfoList, array & $exceptList) {
        echo "Crowl Get {$url}  ";
        Log::write(system::CROWL_LOG, "Crowl Get {$url}  \r\n");
        $links = $node->getLink();
        $links = url::convertRelative($links, $url); //相対URL→絶対URLへ変換
        $links = $this->extractionAvailableLink($links, $exceptList, $url); //クロール対象として有効なリンクを抽出
        echo "+(" . count($links) . ") \r\n";
        $linkInfoListTmp = $this->convertLinkInfo($url, $links); //urlリストをリンク関連情報群に変換
        $linkInfoList = array_merge($linkInfoList, $linkInfoListTmp); //キューに詰め込む
        $this->addExceptList($exceptList, $links); //クロールしないURLに追加
    }

    protected static function decision($info, $parent) {
        //urlステータスによって処理フラグを返す
        switch ($info['status']) {
            case self::HTTP_RESPONSE_200_OK:
            case self::HTTP_RESPONSE_302_FOUND:
                break;
            case self::HTTP_RESPONSE_TIMEOUT_OR_FAILED:
            	return self::DECISION_FAILED;
            default:
                return self::DECISION_SKIP;
                //echo "Crowl SkipLink {$path} -> {$info['status']}\r\n";
        }
        if (isset($info['Content-Type']) === false) {
            return self::DECISION_SKIP;
        }
        switch ($info['Content-Type']) {
            case self::CONTENT_TYPE_HTML:
                //別ドメインのHTMLを取得した場合、除く
                if (url::isCrossDomain($info['url'], $parent) === true) {
                    return self::DECISION_SKIP;
                }
                if (isset($info['Location']) === true) {
                    if (url::isCrossDomain($info['url'], $info['Location']) === true) {
                        return self::DECISION_SKIP;
                    }
                }
                return self::DECISION_CROWL;
            case self::CONTENT_TYPE_OCTET_STREAM:
            case self::CONTENT_TYPE_ZIP:
            case self::CONTENT_TYPE_EXE:
            case self::CONTENT_TYPE_WMV:
            case self::CONTENT_TYPE_MPEG:
            case self::CONTENT_TYPE_MPEG_AUDIO:
                //ダウンロードリンクの場合 DB登録
                return self::DECISION_ENTRY;
            default:
                return self::DECISION_SKIP;
        }
    }
    protected static function addExceptList(array & $exceptList, $links) {
        foreach ($links as $link) {
            $exceptList[$link] = self::CLOWLED;
        }
    }
    protected static function convertLinkInfo($parent, $links) {
        $result = array();
        foreach ($links as $url) {
            $result[] = array(
                'url'    => $url,
                'parent' => $parent,
            );
        }
        return $result;
    }

    protected static function extractionAvailableLink($urlLinks, array & $exceptList, $url = NULL) {
        $result = array();
        foreach ($urlLinks as $link) {
            //既にクロールしている場合、飛ばす
            if (array_key_exists($link, $exceptList) === true) {
                if ($exceptList[$link] === self::CLOWLED) {
                    continue;
                }
            }

            $result[] = $link;
        }
        return $result;
    }
}

で、具体的に何をしているかというと、順に

  • Nodeクラス(HTTPレスポンス=ページ)を管理しているクラスからリンク一覧を取得。
  • 相対パスが含まれている場合、絶対パスに置換
  • 既にクロールしてあったらリンク一覧から削除
  • 最後に残った有効なURLをキューに詰め込む

といったことをしています。

その後、キューから取り出されたURLを元にHTTPヘッダを取得。
HTMLファイルなら上の処理を実行。
ファイルっぽい感じ(octet-stream、video/mpegとか)だったらDLリストに登録しています。

あとはAutoDLバッチとAutoEncodeバッチがよしなにやってくれます。


次は、
・ページから情報を抜き出して作品ごとに分類したい
・DLバッチで1ページに複数あるページだと全てからDLを試みるので、無駄なDL試行を避けたい


tidy関数群が使えるとのことなので、使いたい

クローリング中@動画サイトへの道

先に書いたクローラーを稼働中です。
一応、実装できたのは幅優先のクローリング。
リンクを階層に分けてクロールしていくタイプです。
今多分40万ぐらいのリンク踏んでるハズ…もちろん重複リンクはリンクから落としてあります。
で、クロールロジックって初めて組んだのでロジックがだいぶカオスなことになっているので、組み直したい。



(再構築中)



終了ー
HTTPレスポンスヘッダ周りの処理を考え直したら意外とすっきり。
何してるかっていうと、クロスサイトした場合Content-Typeを調べてtextなら無視。octetstreamとか取得すべきファイルっぽかったらDBに登録しておいて、自動ダウンロードバッチがダウンロードする手順。


ちと、ページをスクレイピングしてタイトルを取り出したいとか、クローリング時点での機能を増やしたいので考案/再構築中です。

無駄にブラウザっぽい挙動をするクラスを作成中

何をしたいかっていうとブラウザからのアクセスしているかのようにクロールしたいんですよね。

ブラウザっぽい挙動といってもHTTPのUserAgentを変えているだけです。
あと、ヘッダからクロール対象かどうか判断しているので、PHPのgetheadersやfileget〜〜関数を利用するとどうしても2リクエストしてしまうので、それは避けたい。
ただ、何気に大変そうなのでモードによってUA変えてLocationがあればもう一回リクエストを投げる程度ですがw

それに伴ってHTTP周りのクラスも作成。
ある程度できた時点で載せるかもです。