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