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

こんにちは。ちょっと間を空けていました。
さて、前回エントリのクローラーですが、試行錯誤の結果、大体(?)完成。
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関数群が使えるとのことなので、使いたい