※この記事は、2019年のアドベントカレンダー「Pyhtonその3」の18日目の記事です。
PythonのWebアプリケーションフレームワークの作成にあたり、WSGIアプリケーションにおけるリクエスト情報の取得について学んだので、まとめます。
FlaskやDjangoのようなWebアプリケーションフレームワークを学ぶ際に、知っておくと役に立つかもしれない内容です。
概要
この記事における「リクエスト情報の取得」とは、ブラウザ等を経由したユーザーからのリクエストをWSGIアプリケーションで受け取り、受け取ったリクエスト情報に含まれるデータをPythonで処理すること、を指しています。
WSGI(ウィズギー)とは
WSGIについては、以前にWerkzeugのチュートリアルの記事で書いたので、詳細については省略します。
端的にいえば、WSGIはWebサーバーと、Pythonのアプリケーションを接続するためのインターフェースです。
WSGIの規約に従って実装することにより、異なる種類のサーバでも、同一のPythonアプリケーションを動作させることができます。
WSGIアプリケーションの作成
はじめに、基本のWSGIアプリケーションを作成します。
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return ['Hello, WSGI'.encode('utf-8')]
これは、以下のWSGIアプリケーションの定義を満たします。
- WSGIアプリケーションは、呼び出し可能なオブジェクトとして定義する。このオブジェクトが呼び出される際、第一引数に環境変数が渡され、第二引数にステータスコードとレスポンスヘッダを受け取る呼び出し可能なオブジェクトが渡される。
- 第二引数に渡されたオブジェクトを呼び出して、ステータスコードとレスポンスヘッダ情報を渡す。
- 戻り値として、バイト文字列をyieldするiterableなオブジェクトを返す。
関数は呼び出し可能なオブジェクトです。そして第一引数にenviron(=環境変数)を受け取り、第二引数にstart_response(=ステータスコードとレスポンスヘッダーを受け取る呼び出し可能なオブジェクト)を受け取ります。
そして、第二引数に渡されたstart_responseを呼び出して、ステータスコード(=200 OK
)とレスポンスヘッダー情報(=[('Content-Type', 'text/html')]
)を渡します。
最後に、戻り値として、バイト文字列をyieldするiterableなオブジェクト(=['Hello, WSGI'.encode('utf-8')]
)を返します。
これで、WSGIアプリケーションの基本形ができました。今回は、第一引数のenvironから、リクエスト情報を取り出して処理する、ということをやっていきます。
environからリクエスト情報を取り出す
環境変数environからリクエスト情報を取り出すのは比較的簡単です。
environ.get('変数名')
で、HTTPリクエストメソッド(例:GET/POST)やクエリ情報、パス情報などの、リクエスト情報を取り出すことができます。
以下は、リクエストメソッドを取得する例です。
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
# メソッド情報を取り出す
method = environ.get('REQUEST_METHOD')
print(method)
return ['Hello, World'.encode('utf-8')]
environ.get('REQUEST_METHOD')
でリクエストメソッドを取得しています。
それでは、リクエストメソッド以外にどのようなリクエスト情報を取得できるかを見てみましょう。下記コードをtest_wsgi_request.py
として保存します。
# test_wsgi_request.py
import cgi
from wsgiref.simple_server import make_server
import pprint
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
pprint.pprint(environ) # environの中身を見やすいように整形して表示
return ['Hello, World'.encode('utf-8')]
with make_server('', 8000, application) as httpd:
httpd.serve_forever()
このコードを実行した後で、http://localhost:8000 にアクセスすると、コンソールにenvironから取得できる情報が一覧で表示されます。非常に長くなるため、一部のみを表示します。
その他の環境変数の詳細はこちらの仕様を確認してください。
{'PATH_INFO': '/',
'REQUEST_METHOD': 'GET',
'QUERY_STRING': '',
'CONTENT_LENGTH': '',
'wsgi.input': <_io.BufferedReader name=764>
...
さて、WEBアプリケーションを作るにあたり取得しておきたい情報は、概ね以下のようなものです。
- リクエストメソッド(GET/POSTなど)
- パス情報(URL:
http://hostname/path?foo=bar
のpath
の部分) - クエリ情報(URL:
http://hostname/path?foo=bar
の?
以降の部分) - リクエストボディ
リクエストメソッドについては、既に書きましたが、他の3つのリクエスト情報についても、同様に取得してみましょう。
パス情報の取得
パス情報の取得も簡単です。先ほどのコード(test_wsgi_request.py
)のapplication
を、以下のように書き換えます。
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
# アクセスされたパスを取得して表示
path_info = environ.get('PATH_INFO')
print('path_info:{}'.format(path_info))
return ['Hello, World'.encode('utf-8')]
http://localhost:8000/test_path にアクセスしてみましょう。コンソールにpath_info:/test_path
と表示されれば、正しくパスを取得できています。
同様に http://localhost:8000/test_wsgi や http://localhost:8000/test_request にもアクセスしてみましょう。異なるパスが表示されるはずです。
クエリ情報の取得
続いて、クエリ情報を取得します。上記のパス情報を取得するコードのうち、environ.get('PATH_INFO')
を
environ.get('QUERY_STRING')
に書き換えるだけです。
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
query = environ.get('QUERY_STRING')
print('query:{}'.format(query))
return ['Hello, World'.encode('utf-8')]
test_wsgi_request.py
を実行し直して、http://localhost:8000/test?test_query にアクセスしてみましょう。
コンソールには、query:test_query
と表示され、URLの?
以降の部分test_query
が取得できていることが分かります。
リクエストボディの取得
リクエストボディの取得は、他のリクエスト情報の取得に比べると、少し厄介です。
application
を下記のように書き換えます。
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
method = environ.get('REQUEST_METHOD')
# contentの長さを取得する
content_length = environ.get('CONTENT_LENGTH', 0)
# 指定した長さの分だけファイルオブジェクトをreadする
if method == 'POST':
body = environ.get('wsgi.input').read(int(content_length))
print('body: {}'.format(body))
return ['Hello, World'.encode('utf-8')]
environ.get('wsgi.input')
でリクエストボディを取得できますが、これはファイルオブジェクトです。そのため、environ.get('CONTENT_LENGHT')
でリクエストボディの長さを取得して、その長さ分だけread()します。
また、GETリクエストの場合には、リクエストボディは存在しないため、POSTリクエストを想定して検証します。コードを保存して、test_wsgi_request.py
を再実行し、curlコマンドでPOSTリクエストを試しましょう。
curl -w '\n' 'http://localhost:8000' --data 'test=test_body' -X POST
このコマンドを実行するとHello, Workd
が返ってくるはずです。そして、test_wsgi_request.py
を実行しているコンソールでは、body: b'test=test_body'
と表示され、リクエストボディを取得できていることが分かります。
curlコマンドの--data
の後のtest=test_body
の部分を書き換えて実行してみると、何をしているかが分かりやすいと思います。
まとめ
この記事では、WSGIアプリケーションを前提に、Pythonでリクエスト情報を処理する方法について書きました。WEBアプリケーションのフレームワークには、今回のようなリクエスト情報の取得を、開発者がより簡単に実装できるようにすること、が求められます。