[BlueLeaf1336]> PROBLEMS> 探求其之弐>

探求其之弐 > WebサーバにGETコマンドを送る

historyTOP

2005/07/12:作成

2005/07/12TOP

続きです(前回分)。前回も書きましたが、処理としてはひたすらページのダウンロードをしまくる、ということになります。そのページがあろうがなかろうが、連番を順番に追いかける、と。実際、エラーの場合でもマイクロソフトのサーバーが、無愛想な「404 Not Found」なページではなく、それなりのページへリダイレクトしてくれます。ただ、問題としては、実在するページについては当然仕方ありませんが、エラーなページについてもそこそこコードの書かれたHTMLページを受けとらなければならないということです。

マイクロソフト技術情報の実在するページのソースTOP

以下のソースコードは、http://support.microsoft.com/kb/256986/JA/の一部(<xmlreader>まで)です。


(改行あり)
<!-- RESG: 7/12/2005 9:01:53 AM - RESX: 1/1/0001 12:00:00 AM -->
<!-- (c) 2005 Microsoft Corporation. All Rights Reserved -->
(改行あり)
<html><head><link type='text/css' rel='Stylesheet' href='/mnpresource/099837dfS.css'><link rel='stylesheet' type='text/css' href='/mnpresource/1e92f11eS.css'><script language='JavaScript'>var doImage=doImage;var TType=TType;function mhHover(tbl,idx,cls){var t,d;if(document.getElementById)t=document.getElementById(tbl);else t=document.all(tbl);if(t==null)return;if(t.getElementsByTagName)d=t.getElementsByTagName("TD");else d=t.all.tags("TD");if(d==null)return;if(d.length<=idx)return;d[idx].className=cls;}function setMSResearch(){var time=new Date();if(document.cookie.indexOf('msresearch=1 ')==-1){document.cookie='msresearch='+time.getTime()+':'+escape(document.location)+':'+escape(document.referrer)+'; path=/; domain=.microsoft.com; ';}}function footerjs(doc){if(doImage==null){var tt=TType==null?"PV":TType;doc.write('<layer visibility="hide"><div style="display:none"><img src="http://c.microsoft.com/trans_pixel.asp?source=localhost&TYPE=' + tt + '&p=MNPGenerator" width=0 height=0 hspace=0 vspace=0 border=0></div></layer>');}if((document.cookie.indexOf('msresearch=1 ')==-1)&&(document.cookie.indexOf('msresearch=')!=-1)){setInterval("setMSResearch()",1000);}}</script><script type="text/javascript" src="/common/script/gsfx/common.js?2"></script><title>Microsoft Windows レジストリの説明</title><meta name="robots" content="nofollow,noarchive"><meta name="KBParents" content="7274 5732 5881 6843 6842 3208 6519 5903 1167 3198 6728 1163 3194 3188 6719 6713 6513 3071 1139 5872 7341 6912 7017 1131 6898 5924 1173 5918 5917 5914 7940 3228 5902 7482 3223 3222 3221 7606 6321 5892 5891 3219 5887 5886 5885 "><meta name="Keywords" content="kbinfo kbregistry kbenv kbfaq KB256986"><meta name="Description" content="Microsoft Windows レジストリについて説明し、その編集方法に関する情報を提供します。"><meta name="MS.LOCALE" content="ja"><meta http-equiv="content-type" content="text/html; charset=utf-8"><xmlreader>

マイクロソフト技術情報の実在しないページのソースTOP

以下のソースコードは、http://support.microsoft.com/kb/1/JA/の一部(<xmlreader>まで)です。


(改行あり)
<!-- RESG: 7/12/2005 8:48:43 AM - RESX: 1/1/0001 12:00:00 AM -->
<!-- (c) 2005 Microsoft Corporation. All Rights Reserved -->
(改行あり)
<html><head><link type='text/css' rel='Stylesheet' href='/mnpresource/099837dfS.css'><link rel='stylesheet' type='text/css' href='/mnpresource/1e92f11eS.css'><script language='JavaScript'>var doImage=doImage;var TType=TType;function mhHover(tbl,idx,cls){var t,d;if(document.getElementById)t=document.getElementById(tbl);else t=document.all(tbl);if(t==null)return;if(t.getElementsByTagName)d=t.getElementsByTagName("TD");else d=t.all.tags("TD");if(d==null)return;if(d.length<=idx)return;d[idx].className=cls;}function setMSResearch(){var time=new Date();if(document.cookie.indexOf('msresearch=1 ')==-1){document.cookie='msresearch='+time.getTime()+':'+escape(document.location)+':'+escape(document.referrer)+'; path=/; domain=.microsoft.com; ';}}function footerjs(doc){if(doImage==null){var tt=TType==null?"PV":TType;doc.write('<layer visibility="hide"><div style="display:none"><img src="http://c.microsoft.com/trans_pixel.asp?source=localhost&TYPE=' + tt + '&p=MNPGenerator" width=0 height=0 hspace=0 vspace=0 border=0></div></layer>');}if((document.cookie.indexOf('msresearch=1 ')==-1)&&(document.cookie.indexOf('msresearch=')!=-1)){setInterval("setMSResearch()",1000);}}</script><script type="text/javascript" src="/common/script/gsfx/common.js?2"></script><title>お探しのサポート技術情報は現在利用できません</title><meta name="robots" content="none"><meta http-equiv="content-type" content="text/html; charset=utf-8"><xmlreader>

それがどうしたのかというとTOP

上の2つのソースコードで、異なっている箇所は赤で表示した所になります。いくつか試してみると、末尾に「/JA/」をつけるかどうかでも内容が変わるような感じです。でも大まかには、上記のような感じで、特に、無効なページかどうかを判定するのに、TITLEタグまで読み取らないとわからないというのはかなりもう一つです。

ちなみに、上のソースコードでは(最後の何十バイトかは実際には不要ですが)、実在する方は、2052バイトあります。実在しない方は、1564バイトです。ほとんどのページがエラーを戻すことを考えると、1500バイト程度をそのたびに読むのはかなり問題となりそうです。

そんなこんなで、ページをダウンロードするのではなくサーバに直接URLが存在するかどうかを問い合わせる、というのを先にやっておこうじゃないかと。そのほうがネットワークにかける負荷が少なそうな気がするし、処理も早そうな予感がします。

前置きが長すぎでしたが、こんな感じになりました。

20050712GetCommand.zip(171,727bytes) 実行ファイルとソースコードです。

実在する方は「200」です。実在しない方は「404」じゃないですね。「302」です。後述のソースコードを見ればわかりますが、ソケットを作ったりしなければならないとはいえ、「HTTP/1.1 200 OK」だけを読み取れば存在が確認できるということになります。ただしそれだけでは面白くないので、ここでは連続する2つの改行(多分、ヘッダと本文を分けるものだと信じているんですが)までを読んでいます。

WebサーバにGETコマンドを送るためのプログラムのソースTOP

本当は一つの関数にできるんですが、何となく分けてみました。というか結果的にユニットファイルになってますが... 初期化部で、WinSockの初期化をやってますが、全くエラーを考慮してません。今まで初期化に失敗したことがないし、失敗してもソケット作ったりするときにちゃんと例外吹くだろうから別にいいやん、というスタンスです。

こける Wired-Winsockを使ってみようぜ-3.住所と氏名では、ホスト名あるいはIPアドレスを整数に変換するときに、片方で試してエラーになったらもう片方を試す、ようなことが説明してあります。そっちの方が堅そうな気がしますが、何となくIPアドレスかどうかを簡単にでもチェックしてみたくなっただけです。

それから、ここでは「改行が2つ見つかったら終了」としてますが、その付近をコメントアウトすれば、ちゃんと全ページデータをダウンロードすることができます。WinInet.dllを使った場合とどっちが早いかは試してませんが、別に早くなる要素もなさそうな気もします。ネットワークの速度に絶対に引っ張られるだろうし。

unit Http;

interface

uses
    Windows, SysUtils, Classes, WinSock;

procedure SendGetCommandToHost(const host, page: string; s: TStream);

implementation

//-----------------------------------------------------------------------------
//  IPアドレス形式の文字列かどうかを調べる
function IsIpText(const s: string): Boolean;

    function IsByteText(const str: string): Boolean;
    var
        j: integer;
    begin
        //  チェック
        if not TryStrToInt(str, j) then Result := false
        //  チェック-2
        else if (j < 0) or (255 < j) then Result := false
        //  通し
        else Result := true;
    end;

var
    l1, l2: integer;
    t, t1: string;
begin
    Result := false;

    //  ピリオドを除去した時に、文字列長が3だけへらないとおかしい
    l1 := Length(s);
    l2 := Length(StringReplace(s, '.', '', [rfReplaceAll]));
    if (l1 <> l2 + 3) then Exit;

    //  最初の数字
    t := s;
    t1 := Copy(t, 1, Pos('.', t) - 1);

    //  チェック
    if not IsByteText(t1) then Exit;

    //  2番目の数字
    Delete(t, 1, Length(t1) + 1);
    t1 := Copy(t, 1, Pos('.', t) - 1);

    //  チェック
    if not IsByteText(t1) then Exit;

    //  3番目の数字
    Delete(t, 1, Length(t1) + 1);
    t1 := Copy(t, 1, Pos('.', t) - 1);

    //  チェック
    if not IsByteText(t1) then Exit;

    //  4番目の数字
    Delete(t, 1, Length(t1) + 1);
    t1 := t;

    //  チェック
    if not IsByteText(t1) then Exit;

    //  通し
    Result := true;
end;

//-----------------------------------------------------------------------------
// IPアドレスかホスト名からIPアドレス整数版を返す
// http://www.asahi-net.or.jp/~nk2w-ishr/winsock3.htm
function GetInetAddr(s: string): integer;
var
    ip : u_long;
    phe: PHostEnt;
begin
    phe := nil;

    //  10進表記の場合
    if IsIpText(s) then
    begin
        ip := inet_addr(PChar(s));
        if (ip <> INADDR_NONE) then phe := gethostbyaddr(@ip, 4, AF_INET);
    end
    //  ホスト名表記の場合
    else
    begin
        phe := gethostbyname(PChar(s))
    end;

    //  だめだった
    if not Assigned(phe) then
    begin
        raise Exception.Create(Format('[%s] IS INVALID ADDRSS.', [s]));
    end;

    //  整数版へ変換
    Result := inet_addr(inet_ntoa(PInAddr(phe^.h_addr_list^)^));
end;

//-----------------------------------------------------------------------------
//  サーバーへのGETコマンド送出と返事取得
//  C/C++300の技(P36)
//  http://www.asahi-net.or.jp/~nk2w-ishr/winsock4.htm
//  http://hp.vector.co.jp/authors/VA009712/take/delphi/kabesys.htm
procedure SendGetCommandToHost(const host, page: string; s: TStream);
var
    GetCommand: string;
    sock: TSocket;
    sockaddr: TSockAddr;
    sendlen: integer;
    recvlen: integer;
    recvdata: array[0..1024] of Char;
    temp: string;
    i: integer;
begin
    try
        //  WEBサーバへ送信する命令
        GetCommand := Format('GET %s HTTP/1.0'#13#10#13#10, [page]);

        //  ソケット作成
        sock := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

        //  作れた?
        if sock = INVALID_SOCKET then
        begin
            raise Exception.Create(SysErrorMessage(GetLastError()));
        end;

        //  接続先のアドレス・ポート指定
        FillChar(sockaddr, SizeOf(TSockAddr), #0);
        sockaddr.sin_family      := AF_INET;
        sockaddr.sin_port        := htons(80);
        sockaddr.sin_addr.S_addr := GetInetAddr(host);

        //  接続
        if connect(sock, sockaddr, sizeof(sockaddr)) = SOCKET_ERROR then
        begin
            closesocket(sock);
            raise Exception.Create(SysErrorMessage(GetLastError()));
        end;

        //  送信
        sendlen := send(sock, GetCommand[1], length(GetCommand), 0);
        if sendlen = SOCKET_ERROR then
        begin
            closesocket(sock);
            raise Exception.Create(SysErrorMessage(GetLastError()));
        end;

        //  受信
        while True do
        begin
            //  初期化
            FillChar(recvdata, SizeOf(recvdata), #0);

            //  受けてみる
            recvlen := recv(sock, recvdata, Length(recvdata), 0);

            //  エラーなら終わり
            if recvlen = SOCKET_ERROR then break;

            //  空なら終わり
            if recvlen = 0 then break;

            //  改行2連続で中断
            i := AnsiPos(#13#10#13#10, recvdata);
            if (i > 0) then
            begin
                //  改行を除去して
                temp := recvdata;
                temp := Copy(temp, 1, i - 1);

                //  書き込み
                s.Write(temp[1], Length(temp));

                //  終了
                break;
            end
            else
            begin
                //  書き込み
                s.Write(recvdata, recvlen);
            end;
        end;

        // 巻き戻し
        s.Position := 0;

        // ソケットの破棄
        closesocket(sock);
    except
        on E:Exception do
        begin
            raise Exception.Create(E.Message);
        end;
    end;
end;


//-----------------------------------------------------------------------------
//  WinSock初期化
var
    ad: TWSAData;

initialization
    WSAStartup($0101, ad);

//-----------------------------------------------------------------------------
//  後始末
finalization
    WSACleanup();

end.

使い方TOP

こんな感じで。別にTStreamから派生しているものなら何でもかまいません。多分動くと思いますが、実際のソースコードをテキストエディタで適当に整形(変数を無くしたり)したので動かないかもしれません。どうしても動かしてみたいという方は、上の方にあるファイルをダウンロードしてください。

procedure TForm1.Button1Click(Sender: TObject);
var
    ss: TStringStream;
begin
    ss := nil;
    try
        //  文字列ストリームに結果を受けとる
        ss := TStringStream.Create('');
        //  実行
        SendGetCommandToHost('www.google.com', '/', ss);
        //  表示
        ShowMessage(ss.DataString);
        //  解放
        FreeAndNil(ss);
    except
        on E:Exception do ShowMessage(E.Message);
    end;
end;

というわけで、異常に縦に長いページができました。

WebサーバにURLの実在を問い合わせて、「200」以外が返ったらそのURLはスキップする、そうでなければページをダウンロードする、という流れにできそうです。ただ、今のところは全ページをダウンロードしてどうする? と考えてます。以下の3つ

で充分かと。また、これに絞ることで必要な箇所だけダウンロードすればよくなります。最初に示したソースコード内に含まれているので、およそ2000バイトと考えてよいと思います。数が数だけにそんな感じにしておかないと、サーバに怒られて蹴られてアクセスできなくなる気もします。

ということで、次回はURLからタイトルと説明を(マイクロソフトのサイト限定仕様で)抽出するところをやりたいです。

EOFTOP