fsockopen()とHTTP/1.1

キーワード密度やタグ解析をするSEOツールっぽいものを作ろうと思いHTTPクライアントを作ってみました。

function createRequest($url, $method = 'GET', $header = '', $posts = array())
{
    $uri = parse_url($url);
    
    if (isset($uri['query'])) {
        $uri['query'] = '?' . $uri['query'];
    } else {
        $uri['query'] = '';
    }
    //デフォルト
    $out  = $method . ' ' . $uri['path'] . $uri['query'] . " HTTP/1.1\r\n";
    $out .= "Host: " . $uri['host'] . "\r\n";
    $out .= "User-Agent: PHP/" . phpversion() . "\r\n";
    $out .= "Connection: Close\r\n";
    $out .= $header;
    
    //POSTの場合
    if ($method == 'POST') {
        foreach ($posts as $key => $val) {
            $post[] = $key . '=' . $val;
        }
        $postdata = implode('&', $post);
        $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
        $out .= "Content-Length: " . strlen($postdata) . "\r\n\r\n";
        $out .= $postdata;
    } else {
        $out .= "\r\n";
    }
    return $out;
}

リクエストヘッダーの作成
HTTP/1.1で通信するとチャンク形式になってしまいところどころにchunk-sizeが書き込まれてしまいます。
デコード書くの面倒だしどこかにないかと検索したけど見つからず。
HTTP/1.0でいいかなぁ、と思っていたら
ちゃんとfsockopen()のマニュアルのところにデコード方法がコメントされていました。(後述)

function getResponse($url, $out)
{
    $uri = parse_url($url);
    if (!isset($uri['port'])) $uri['port'] = 80;
    //WEBサーバへ接続
    $fp = @fsockopen($uri['host'], $uri['port'], $errno, $errstr, 30)
        or die('[' . $errno . ']' . $errstr);
    //リクエストの送信
    fwrite($fp, $out);
    //レスポンスヘッダーの受信
    $in = '';
    //<CRLF>が2つ続くまでループ
    do {
        $in .= fgets($fp, 4096);
    } while (strpos($in, "\r\n\r\n") === false);
    //ヘッダーを配列に格納
    $info = decode_header($in);
    
    //コンテンツ部分の受信
    $in = '';
    while (!feof($fp)) {
        $in .= fgets($fp, 4096);
    }
    fclose($fp);
    //デコード
    $body = decode_body($info, $in);
    
    return array($info, $body);
}

WEBサーバとの通信

function decode_header($str)
{
    //<CRLF>ごとに分割
    $part = preg_split("/\r\n/", $str, -1, PREG_SPLIT_NO_EMPTY);
    $out = array ();
    for ($h = 0; $h < sizeof($part); $h++) {
        if ($h != 0) {
            // :で区切ってkeyとvalueを作成
            $pos = strpos($part[$h], ':');
            $k = strtolower(str_replace(' ', '', substr($part[$h], 0, $pos)));
            $v = trim(substr($part[$h], ($pos + 1)));
        } else {
            //1行目ステータスコード
            $k = 'status';
            $v = explode (' ', $part[$h]);
            $v = $v[1];
        }
        //keyとvalueを配列に格納
        if ($k == 'set-cookie') {
                $out['cookies'][] = $v;
        } elseif ($k == 'content-type') {
            if (($cs = strpos($v, ';')) !== false) {
                //目的が解析なのでサブタイプは切り捨てない
                //$out[$k] = substr($v, 0, $cs);
                $out[$k] = $v;
            } else {
                $out[$k] = $v;
            }
        } else {
            $out[$k] = $v;
        }
    }
    return $out;
}

ヘッダーを配列に格納マニュアルのjbr at ya-right dot comさん
これでchunkedかどうか解りやすくなりました。

function decode_body ($info, $str, $eol = "\r\n")
{
    $tmp = $str;
    $add = strlen($eol);
    $str = '';
    //チャンク形式の判定
    if (isset($info['transfer-encoding']) && $info['transfer-encoding'] == 'chunked') {
        do {
            //チャンクサイズ取得してを10進数に変換
            $tmp = ltrim($tmp);
            $pos = strpos($tmp, $eol);
            $len = hexdec(substr($tmp, 0, $pos));
            //圧縮転送されている場合解凍する
            if (isset($info['content-encoding'])) {
                $str .= gzinflate(substr($tmp, ($pos + $add + 10), $len));
            } else {
                $str .= substr($tmp, ($pos + $add), $len);
            }
            $tmp = substr($tmp, ($len + $pos + $add));
            $check = trim($tmp);
        } while (!empty($check));
    } elseif (isset($info['content-encoding'])) {
        //圧縮転送されている場合解凍する
        $str = gzinflate(substr ($tmp, 10));
    }
    return $str;
}

コンテンツ部分のデコード同じくマニュアルのjbr at ya-right dot comさん


これで簡単なHTTPクライアントとして機能します。
指定URLのページの解析以外にもGETやPOST埋め込んで検索結果取り出したりできるかな
でも、Googleとかツールを使ってのアクセスとか規約的にどうなんだろう?


何よりもまずマニュアルを読む、これが大事ですね^^;