中学理科1626207 views
高校日本史189857 views
いろは2986023 views
高校物理158224 views
小学社会308636 views
英語607877 views
高校生物549842 views
りんご192546 views
小学理科717236 views
中学社会667106 views
Help
Tools

English

WSGI アプリケーションを自作する

WSGI(Web Server Gateway Interface)の仕様を理解する最良の方法は、実際に動くアプリケーションを自分で書いてみることだ。フレームワークを使わず、素の Python だけで WSGI アプリケーションを構築する過程を通じて、Web アプリケーションの本質的な動作原理が見えてくる。

最小限の WSGI アプリケーション

WSGI アプリケーションの実体は、呼び出し可能なオブジェクト(callable)である。関数でもクラスでも、呼び出し可能であれば何でも構わない。

def application(environ, start_response):
    status = '200 OK'
    headers = [('Content-Type', 'text/plain; charset=utf-8')]
    start_response(status, headers)
    return [b'Hello, WSGI!']

このたった 5 行のコードが、完全に動作する WSGI アプリケーションだ。environ はリクエスト情報を含む辞書、start_response はレスポンスヘッダーを設定するためのコールバック関数である。戻り値はレスポンスボディをバイト列のイテラブルとして返す。

標準ライブラリの wsgiref を使えば、このアプリケーションをすぐに動かせる。

from wsgiref.simple_server import make_server

def application(environ, start_response):
    status = '200 OK'
    headers = [('Content-Type', 'text/plain; charset=utf-8')]
    start_response(status, headers)
    return [b'Hello, WSGI!']

if __name__ == '__main__':
    server = make_server('localhost', 8000, application)
    print('Serving on http://localhost:8000')
    server.serve_forever()

python app.py を実行し、ブラウザで http://localhost:8000 にアクセスすれば「Hello, WSGI!」と表示される。

ルーティングの実装

実用的なアプリケーションには、URL に応じて異なる処理を行うルーティング機能が必要になる。environ['PATH_INFO'] からリクエストパスを取得し、条件分岐で処理を振り分ける。

def application(environ, start_response):
    path = environ['PATH_INFO']
    
    if path == '/':
        status = '200 OK'
        body = b'Welcome to the homepage!'
    elif path == '/about':
        status = '200 OK'
        body = b'About this application'
    elif path == '/users':
        status = '200 OK'
        body = b'User list'
    else:
        status = '404 Not Found'
        body = b'Page not found'
    
    headers = [('Content-Type', 'text/plain; charset=utf-8')]
    start_response(status, headers)
    return [body]

この方法は単純だが、パスが増えると管理が困難になる。辞書を使ったルーティングテーブルを導入すれば、より整理された構造になる。

def home(environ):
    return '200 OK', b'Welcome to the homepage!'

def about(environ):
    return '200 OK', b'About this application'

def users(environ):
    return '200 OK', b'User list'

routes = {
    '/': home,
    '/about': about,
    '/users': users,
}

def application(environ, start_response):
    path = environ['PATH_INFO']
    handler = routes.get(path)
    
    if handler:
        status, body = handler(environ)
    else:
        status = '404 Not Found'
        body = b'Page not found'
    
    headers = [('Content-Type', 'text/plain; charset=utf-8')]
    start_response(status, headers)
    return [body]

ハンドラー関数を分離したことで、各エンドポイントの処理を独立して記述できるようになった。

HTTP メソッドの判別

RESTful な API を構築するには、GET / POST / PUT / DELETE などの HTTP メソッドを判別する必要がある。environ['REQUEST_METHOD'] でメソッドを取得できる。

def users_handler(environ):
    method = environ['REQUEST_METHOD']
    
    if method == 'GET':
        return '200 OK', b'{"users": ["alice", "bob"]}'
    elif method == 'POST':
        return '201 Created', b'{"message": "User created"}'
    elif method == 'DELETE':
        return '200 OK', b'{"message": "User deleted"}'
    else:
        return '405 Method Not Allowed', b'{"error": "Method not allowed"}'

メソッドとパスの組み合わせでルーティングするには、タプルをキーとした辞書を使う方法がある。

routes = {
    ('GET', '/'): home_get,
    ('GET', '/users'): users_get,
    ('POST', '/users'): users_post,
    ('DELETE', '/users'): users_delete,
}

def application(environ, start_response):
    method = environ['REQUEST_METHOD']
    path = environ['PATH_INFO']
    key = (method, path)
    
    handler = routes.get(key)
    # 以下略

リクエストボディの読み取り

POST リクエストなどで送信されたデータは、environ['wsgi.input'] から読み取る。このオブジェクトはファイルライクオブジェクトであり、read() メソッドでバイト列を取得できる。

def get_request_body(environ):
    try:
        content_length = int(environ.get('CONTENT_LENGTH', 0))
    except ValueError:
        content_length = 0
    
    if content_length > 0:
        return environ['wsgi.input'].read(content_length)
    return b''

CONTENT_LENGTH が設定されていない場合や不正な値の場合に備えて、エラーハンドリングを入れている。JSON データを受け取る場合は、さらにパースが必要になる。

import json

def users_post(environ):
    body = get_request_body(environ)
    try:
        data = json.loads(body.decode('utf-8'))
        name = data.get('name', 'Unknown')
        return '201 Created', json.dumps({'created': name}).encode('utf-8')
    except json.JSONDecodeError:
        return '400 Bad Request', b'{"error": "Invalid JSON"}'

クエリパラメータの解析

URL のクエリ文字列(?key=value&foo=bar の部分)は environ['QUERY_STRING'] に格納されている。標準ライブラリの urllib.parse を使って解析できる。

from urllib.parse import parse_qs

def search(environ):
    query_string = environ.get('QUERY_STRING', '')
    params = parse_qs(query_string)
    
    # params は {'key': ['value1', 'value2'], ...} の形式
    keyword = params.get('q', [''])[0]
    page = params.get('page', ['1'])[0]
    
    result = f'Search: {keyword}, Page: {page}'
    return '200 OK', result.encode('utf-8')

parse_qs は同じキーが複数回現れる可能性を考慮して、値をリストで返す。単一の値が欲しい場合は [0] でアクセスするか、parse_qs(query_string, keep_blank_values=True) のオプションを調整する。

クラスベースのアプリケーション

関数ベースのアプリケーションが大きくなってきたら、クラスにまとめると管理しやすくなる。__call__ メソッドを実装すれば、インスタンスを WSGI アプリケーションとして使える。

class Application:
    def __init__(self):
        self.routes = {}
    
    def route(self, path, methods=None):
        if methods is None:
            methods = ['GET']
        
        def decorator(func):
            for method in methods:
                self.routes[(method, path)] = func
            return func
        return decorator
    
    def __call__(self, environ, start_response):
        method = environ['REQUEST_METHOD']
        path = environ['PATH_INFO']
        
        handler = self.routes.get((method, path))
        
        if handler:
            status, body = handler(environ)
        else:
            status = '404 Not Found'
            body = b'Not Found'
        
        headers = [('Content-Type', 'text/plain; charset=utf-8')]
        start_response(status, headers)
        return [body]

app = Application()

@app.route('/')
def home(environ):
    return '200 OK', b'Home'

@app.route('/users', methods=['GET', 'POST'])
def users(environ):
    if environ['REQUEST_METHOD'] == 'GET':
        return '200 OK', b'User list'
    else:
        return '201 Created', b'User created'

デコレータを使ったルーティング登録は、Flask などのフレームワークでおなじみのパターンだ。このように、フレームワークが提供する便利な機能も、根底では WSGI の仕組みの上に構築されている。

まとめ

WSGI アプリケーションは、environstart_response を受け取り、レスポンスボディのイテラブルを返すという単純なインターフェースに基づいている。

最小構成

関数一つで WSGI アプリケーションを作れる。start_response でステータスとヘッダーを設定し、バイト列のリストを返すだけ。

拡張のポイント

ルーティング、メソッド判別、リクエストボディ解析、クエリパラメータ処理を追加することで、実用的なアプリケーションに成長させられる。

フレームワークを使わずに WSGI アプリケーションを書く経験は、Flask や Django の内部で何が起きているかを理解する土台となる。