何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

『Real World HTTP』with Python

requestsを使ったら負けかなと思っている

『Real World HTTP』を読み始めたが、自分でも実際に書いてみるのもよいと思うのでPythonで書いてみた。 PythonのHTTPクライアントというと標準モジュールurllib.requestではなくrequestsが主流であるが、requestsをそのまま使うのでは勉強にならないので今回は標準モジュールの範囲内で頑張る。 「電池付き」であるPythonで実行するという時点ですでに「勉強にならない」可能性もあるが今回はHTTPを学ぶので一旦忘れよう。

www.oreilly.co.jp

1.1.1 テストエコーサーバーの実行

あるディレクトリをルートにしたHTTPサーバーを実行する場合は単純にpython -m http.server 18888とすれば実行できる。 今回はエコーサーバーであるのでhttp.serverにあるクラスを使って組み立てる必要がある。 ドキュメントには詳しい方法が書いていないので少し難儀した。 以下の例のようにdo_*メソッドを実装する必要がある。

import datetime
import http
import http.server


class HTTPRequestHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if "Cookie" in self.headers:
            body = b"<html><body>Hello</body></html>"
        else:
            body = b"<html><body>Welcome</body></html>"

        self.send_response(http.HTTPStatus.OK)
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.send_header('Content-length', len(body))
        self.send_header('Set-Cookie', "LAST_ACCESS={}".format(datetime.datetime.now().isoformat()))
        self.end_headers()
        self.wfile.write(body)

    def do_HEAD(self):
        body = b"<html><body>hello</body></html>"
        self.send_response(http.HTTPStatus.OK)
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.send_header('Content-length', len(body))
        self.end_headers()

    def do_POST(self):
        self.data_string = self.rfile.read(int(self.headers['Content-Length']))
        self.send_response(http.HTTPStatus.OK)
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.send_header('Content-length', len(self.data_string))
        self.end_headers()
        self.log_message(self.data_string.decode("utf8"))


def run(server_class=http.server.HTTPServer, handler_class=HTTPRequestHandler):
    server_address = ('', 18888)
    httpd = server_class(server_address, handler_class)
    httpd.serve_forever()


if __name__ == "__main__":
    run()

POSTメソッドの実装が若干面倒であるが、Content-Lengthが必要となることは『Real World HTTP』に書いてある。 また、Cookieの取り扱いも行いたいのでGETのヘッダーにCookieを入れるようにしてある。

3.4 GETメソッドの送信と、ボディ、ステータスコード、ヘッダーの受信

Pythonの標準モジュールでGETメソッドを実行する場合はurllib.requestモジュールのurlopenを使う。

import urllib.error
import urllib.request


def main():
    try:
        with urllib.request.urlopen("http://localhost:18888") as response:
            html = response.read()
            print("Status Code:", response.code)
            print("Reason:", response.reason)
            print(response.headers)
            print(html.decode("utf8"))
            

    except urllib.error.HTTPError as err:
        print(err)

    except urllib.error.URLError as err:
        print(err)


if __name__ == "__main__":
    main()
Status Code: 200
Reason: OK
Server: BaseHTTP/0.6 Python/3.5.3
Date: Sat, 22 Jul 2017 05:21:48 GMT
Content-type: text/html; charset=utf-8
Content-length: 31


<html><body>hello</body></html>

3.5 GETメソッド+クエリーの送信

Pythonの標準モジュールでクエリ文字列のURLエンコードを行う場合はurllib.parseurlencode函数を使う。

import urllib.error
import urllib.parse
import urllib.request


def main():
    try:
        data = urllib.parse.urlencode({"query": "hello world"})
        url = "http://localhost:18888" + "?" + data
        with urllib.request.urlopen(url) as response:
            html = response.read()
            print("Status Code:", response.code)
            print("Reason:", response.reason)
            print(response.headers)
            print(html.decode("utf8"))


    except urllib.error.HTTPError as err:
        print(err)

    except urllib.error.URLError as err:
        print(err)


if __name__ == "__main__":
    main()

このスクリプトを実行すると、エコーサーバー側のログに次のような結果がロギングされる。

127.0.0.1 - - [22/Jul/2017 15:01:36] "GET /?query=hello+world HTTP/1.1" 200 -

3.6 HEADメソッドでヘッダーを取得

本文では、

HEADメソッドの実装は簡単です。http.Getの代わりにhttp.Head()を利用するだけです。

とあるように、メソッドをすり替えるだけでよいが、Pythonの標準モジュールの場合はそう簡単にはことが進まない。 GETやPOSTの場合はurllib.request.urlopenに直接URL(文字列)を渡せばよいが、PUTやHEADの場合はurllib.request.Requestオブジェクトを渡す必要がある。 urllib.request.Requestオブジェクトを生成する際にHTTPメソッドを指定する。

import urllib.error
import urllib.parse
import urllib.request


def main():
    try:
        request = urllib.request.Request(url="http://localhost:18888", method="HEAD")
        with urllib.request.urlopen(request) as response:
            print("Status Code:", response.code)
            print("Reason:", response.reason)
            print(response.headers)


    except urllib.error.HTTPError as err:
        print(err)

    except urllib.error.URLError as err:
        print(err)


if __name__ == "__main__":
    main()

なお、本文にはhttp.Getとあるが、正しくはhttp.Get()と表記するべきだと思う。

3.7 x-www-form-urlencoded形式のPOSTメソッドの送信

POSTで送信する内容は事前にBytesに変換する必要がある。ドキュメントを参照。

import urllib.error
import urllib.parse
import urllib.request


def main():
    try:
        data = urllib.parse.urlencode({'test': "value"})
        data = data.encode("ascii")
        with urllib.request.urlopen("http://localhost:18888", data) as response:
            print("Status Code:", response.code)
            print("Reason:", response.reason)
            print(response.headers)


    except urllib.error.HTTPError as err:
        print(err)

    except urllib.error.URLError as err:
        print(err)


if __name__ == "__main__":
    main()

3.10 クッキーの送受信

標準モジュールでCookieを扱うには複雑な手順を踏む。 まず、Cookieを保存するhttp.cookiejar.CookieJarクラスのインスタンスjarを作成し、そのインスタンスを備えるurllib.request.OpenerDirectorクラスのインスタンスopenerを作成する。 urllib.request.urlopenの代わりにopeanerでURLにアクセスする。 複雑ではあるものの、Go言語の場合と似たような手順を踏んでいることがわかる。

import http.cookiejar
import urllib.error
import urllib.request


def main():
    jar = http.cookiejar.CookieJar()
    opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
    for _ in range(2):
        try:
            with opener.open("http://localhost:18888") as response:
                html = response.read()
                print("Status Code:", response.code)
                print("Reason:", response.reason)
                print(response.headers)
                print(html.decode("utf8"))
                print()

        except urllib.error.HTTPError as err:
            print(err)

        except urllib.error.URLError as err:
            print(err)


if __name__ == "__main__":
    main()

クライアント側の出力は以下の通り。

Status Code: 200
Reason: OK
Server: BaseHTTP/0.6 Python/3.5.3
Date: Sat, 22 Jul 2017 15:48:44 GMT
Content-type: text/html; charset=utf-8
Content-length: 33
Set-Cookie: LAST_ACCESS=2017-07-23T00:48:44.741324


<html><body>Welcome</body></html>

Status Code: 200
Reason: OK
Server: BaseHTTP/0.6 Python/3.5.3
Date: Sat, 22 Jul 2017 15:48:45 GMT
Content-type: text/html; charset=utf-8
Content-length: 31
Set-Cookie: LAST_ACCESS=2017-07-23T00:48:45.761960


<html><body>Hello</body></html>

3.13 自由なメソッドの送信 及び 3.14 ヘッダーの送信

urllib.request.Requestオブジェクトを生成する際にヘッダーやメソッドを指定することができる。

3.15 国際化ドメイン

Pythonの場合は文字列のエンコーディングidnaを指定すればよい。

def main():
    src = "日本語ドメイン.jp"
    idna_src = src.encode("idna")
    print("{} -> {}".format(src, idna_src))


if __name__ == "__main__":
    main()

出力結果は以下の通りになる。

日本語ドメイン.jp -> b'xn--eckwd4c7c5976acvb2w6i.jp'