PythonのsocketでHTTPSクライアントを作る
- 作成日: 2020-12-08
- 更新日: 2023-12-24
- カテゴリ: Python
socketでHTTPSクライアントを作る
最近のWebサイトはみんなHTTPSになってますね。
Pythonのsocket
モジュールを使うとソケット通信を行うプログラムを作ることが出来ます。
このソケット通信でHTTPSのページのコンテンツを取得するプログラムを今回は作ってみようと思います。
HTTPとHTTPSではソケット通信のコードの一部を変更する必要がありますが、今回解説するのはHTTPSです。
WebページにHTTP(HTTPS)のリクエストを送信して、そのページのコンテンツを取得し、画面に表示するという具合です。
具体的には↓を見ていきます。
- HTTPの概要
- ソケットをSSLに対応させる方法
- HTTP(HTTPS)クライアントの作成
HTTPの概要
ホストとホストの間の通信で使われる約束事を「プロトコル」と言います。
WebサーバーとWebクライアントのあいだで使われるのは「HTTP」というプロトコルです。
HTTPとはまずクライアントがサーバーに「HTTPリクエスト」を送信します。
HTTPリクエストには、サーバーの「どこの場所のコンテンツ」に対して「どんなメソッドを使うか」などが書かれています。
サーバーはクライアントが送信したHTTPリクエストを受信し、解析します。
そしてメソッドを判断し、指定された場所に対してそのメソッドの処理を適用します。
たとえばメソッドが「GET」で場所が「/hige」であれば「/higeのコンテンツを取得する」という具合です。
サーバーは指定されたメソッドの実行が完了したら、その結果を「HTTPレスポンス」としてクライアントに返します。
この時クライアントはサーバーからデータを読み込んでいる状態です。
つまり通信の順番的には↓のようになっているわけです。
- クライアントがサーバーにHTTPリクエストを送信する
- サーバーがクライアントからHTTPリクエストを読み込む
- サーバーがHTTPリクエストを解析し、メソッドを実行する
- サーバーがクライアントへHTTPレスポンスを送信する
- クライアントがサーバーからHTTPレスポンスを読み込む
このようなやり取りを経て、サーバーとクライアントはリクエストとレスポンスを交換します。
クライアントがWebブラウザであれば、レスポンスを解析して画面にグラフィカルな表示を行います。
SSLを使ったソケットの通信でも↑の基本的なやり取りは一緒です。
SSLの関連の処理はPythonのssl
というモジュールが隠蔽してくれて、開発者は特に意識する必要もありません。
気をつけたいのはHTTPのデフォルトポートが80
であるのに対し、HTTPSは443
であるというところです。
ソケットをSSLに対応させる方法
ソケットの作成はPythonのsocket
モジュールを使います。
このソケットではデフォルトではSSLに対応していません。
そのためこのソケットをSSLでラップする必要があります。
Pythonのssl
というモジュールを使うとsocket
で作成したソケットをSSLでラップすることが出来ます。
つまりHTTPSなWebサイトとソケット通信を行いたい場合は、↓のモジュールが必要になるわけです。
- socket
- ssl
ssl
モジュールを使うと非常に簡単にSSLを利用したソケットを作成することが出来ます。
HTTP(HTTPS)クライアントの作成
ではHTTP(HTTPS)クライアントを作成します。
↓がコード全文です。
import socket
import ssl
HOST = 'this.is.my.host.com' # 通信先のホスト(適当に変えてください)
PORT = 443 # 通信に使うポート
def get_root(ssock):
"""
HOSTの/からHTTPを使用してコンテンツを読み込む
"""
# HTTPリクエスト・ヘッダーを作成
msg = b'GET / HTTP/1.1\r\n' # /のコンテンツを取得する
msg += b'Host: ' + HOST.encode() + b'\r\n' # ホストの指定
msg += b'Connection: close\r\n' # 接続後に閉じる
msg += b'\r\n' # <- この改行がおわりの合図
# サーバーにリクエストを送信
ssock.send(msg)
# サーバーからレスポンスを読み込み
chunks = [] # チャンクのリスト
while True:
chunk = ssock.recv(1024) # 1024バイトずつ読み込む
if chunk == b'': # 結果が空ならおわり
break
chunks.append(chunk) # 読み取ったチャンクを追加
# チャンクのリストを文字列に変換
s = ''
for chunk in chunks:
# UTF-8固定でバイト列を文字列に変換
# 'ignore'でエラーを無視する
s += chunk.decode('utf-8', 'ignore')
# OK
return s
def main():
# SSLコンテキストを作成
context = ssl.create_default_context()
# 普通のソケットを作成
with socket.create_connection((HOST, PORT)) as sock:
# コンテキストでソケットをラップする
with context.wrap_socket(sock, server_hostname=HOST) as ssock:
# /のコンテンツを取得する
result = get_root(ssock)
# 結果を出力
print(result)
# main関数の呼び出し
main()
順を追って↓で解説していきます。
main関数内の処理
最初に実行されるのはmain
関数で、↓のようにコードの最後の方で呼び出しています。
# main関数の呼び出し
main()
main
関数の中では↓のように最初にSSLのコンテキストを作成します。
# SSLコンテキストを作成
context = ssl.create_default_context()
このコンテキストを使うことでソケットをラップすることが出来ます。
次に↓のようにソケット(sock
)を作成します。
# 普通のソケットを作成
with socket.create_connection((HOST, PORT)) as sock:
...
socket.create_connection()
の引数にはタプルを渡します。
タプルの第1要素には接続先のアドレス、第2要素にはポート番号を渡します。
これらのHOST
とPORT
はコードの先頭の方に↓のように書いてあります。
HOST = 'this.is.my.host.com' # 通信先のホスト(適当に変えてください)
PORT = 443 # 通信に使うポート
↑のようにHOST
には文字列で通信先のホストのアドレスを指定します。
これはたとえばURLが「https://this.is.my.host.com/hige
」なら「this.is.my.host.com
」の部分がHOST
です。
ポート番号にはHTTPSのデフォルトポート番号である443
を指定します。
次に作成したソケット(sock
)を↓のようにSSLでラップします。
# コンテキストでソケットをラップする
with context.wrap_socket(sock, server_hostname=HOST) as ssock:
...
↑のコードで作成されたssock
はSSLでラップされたソケットです。
このソケットを使うことでリクエストの送信とレスポンスの受信を行うことが出来ます。
次にラップしたソケット(ssock
)を使ってWebページのコンテンツを取得します。
コンテンツの取得は↓のようにget_root()
という関数で行います。
# /のコンテンツを取得する
result = get_root(ssock)
↑のようにget_root()
はssock
を引数に取り、結果を文字列で返します。
つまり↑のresult
にはWebページのコンテンツが代入されます。
get_root関数の処理
get_root()
の処理を見てみます。
HTTPリクエストの作成
get_root()
では↓のように最初にHTTPリクエストを作成します。
# HTTPリクエスト・ヘッダーを作成
msg = b'GET / HTTP/1.1\r\n' # /のコンテンツを取得する
msg += b'Host: ' + HOST.encode() + b'\r\n' # ホストの指定
msg += b'Connection: close\r\n' # 接続後に閉じる
msg += b'\r\n' # <- この改行がおわりの合図
ソケット通信でやり取りされるデータはバイト列です。
そのためHTTPリクエストも↑のようにバイト列で作成します(b'text'
などがバイト列です)。
HTTPリクエストのフォーマットで気をつけたいのは、リクエスト内の改行はCRLF
で書くことです。
↑の場合\r\n
がCRLF
の改行です。
リクエストの最初の行に↓のようにメソッドとメソッドを適用させる場所、それからHTTPのバージョンを書きます。
GET
がメソッドで、/
が場所、HTTP/1.1
がHTTPのバージョンです。
GET
は「取得」を表すメソッドです。つまり「/
のコンテンツを取得(GET)したい」というリクエストになります。
プロトコルにもバージョンがあるので↓のように明示的に指定します。
msg = b'GET / HTTP/1.1\r\n' # /のコンテンツを取得する
↓のようにHost:
の行には通信先のホスト名を書きます。これはHOST
変数の値を流用しています。
msg += b'Host: ' + HOST.encode() + b'\r\n' # ホストの指定
↓のようにConnection:
の行には現在のトランザクションが完了した後にネットワーク接続を継続するか指定します。
close
を指定するとトランザクションが完了したとに接続が閉じます。
これを指定しないとsocket.recv()
で延々に受信することになるので注意が必要です。
msg += b'Connection: close\r\n' # 接続後に閉じる
↓のようにHTTPリクエストのヘッダーの最後に改行を入れておきます。
この改行がヘッダーの終端です。つまりこの改行のあとはリクエストのボディ部分になり、POSTのデータなどはここにデータが書かれます。
msg += b'\r\n' # <- この改行がヘッダー部分のおわりの合図
サーバーにHTTPリクエストを送信
HTTPリクエストを作成したらソケットのsend()
メソッドを使ってサーバーにリクエストを送信します。
# サーバーにリクエストを送信
ssock.send(msg)
サーバーからレスポンスを読み込む
リクエストを送信したら次にサーバーからレスポンスを読み込みます。
読み込みにはソケットのrecv()
メソッドを使います。
このメソッドの第1引数には読み込みの最大バイト数を指定します。
読み込んだデータはchunk
としてchunks
リストに追加していきます。
recv()
が空のバイト列を返して来たら読み込むデータが無いのでループからブレークします。
こうすることでchunks
リストに読み込んだバイト列が格納されていきます。
# サーバーからレスポンスを読み込み
chunks = [] # チャンクのリスト
while True:
chunk = ssock.recv(1024) # 1024バイトずつ読み込む
if chunk == b'': # 結果が空ならおわり
break
chunks.append(chunk) # 読み取ったチャンクを追加
chunksを文字列に変換
chunks
はリストで、中身はバイト列です。
これを文字列に変換します。
バイト列のメソッドdecode()
を使うと文字列に変換できます。
エンコーディングはutf-8
を指定し、エラーが発生したら無視するようにignore
を指定します。
utf-8
固定になっているのでsjis
のページを読み込みたい場合はここのエンコーディングを変更してください。
# チャンクのリストを文字列に変換
s = ''
for chunk in chunks:
# UTF-8固定でバイト列を文字列に変換
# 'ignore'でエラーを無視する
s += chunk.decode('utf-8', 'ignore')
最後に変換した文字列をreturn
します。
# OK
return s
これでget_root()
はおわりです。
実行してみよう
このスクリプトを保存しPythonで実行すると、HOST
のサーバーに対してHTTP通信を行います。
そしてルート(/
)のページのコンテンツが画面に出力されます。
このようにsocket
とssl
モジュールを使うと比較的に簡単にHTTPSのページを取得することが可能です。
🦝 < socketとsslの戯れ