2008年8月31日日曜日

WSGIことはじめ

WSGI (Web Server Gateway Interface) は Django や Google App Engine を始めたくさんの Python Web フレームワークが準拠している仕様で、これについての秀逸なチュートリアルの翻訳。日本語訳が秀逸かどうかは知らないので、怪しい部分は原文にあたって教えてください。


WSGI ことはじめ
====================
著者(Author): Armin Ronacher
原文(Original Text): http://lucumr.pocoo.org/articles/getting-started-with-wsgi
翻訳(Japanese Translation): hkurosawa
ライセンス(Lisence): http://creativecommons.org/licenses/by-nc-sa/2.0/at/deed.ja
====================

やっと論文も終わって、プロジェクトや記事を書くための時間ができた。
ずっと書きたかったものの一つが、特定のフレームワークや実装を
必要としない WSGI チュートリアルなんだ。さあ始めよう。

wsgi-snake.png

* WSGI ってなんだ?
基本的に、WSGI はたぶんキミのしってる CGI よりも低層のものだ。
だけど CGI と違うのは、WSGI はスケールするし、マルチスレッドでも
マルチプロセス環境でも動作する、なぜって、これはどう実装されるかは
ぜんぜん気にしてない仕様だからだ。事実、 WSGI は Web アプリケーションと
ウェブサーバの CGI、mod_python、FastCGI または wsgiref と呼ばれる
コアに WSGI を組み込んだ Python 標準ライブラリのスタンドアロンサーバ
みたいな層の間に位置して、だから CGI とは異なる。

WSGI は PEP 333 で定義されていて、有名な django や pylons を含む
様々なフレームワークに適用されている。

キミがサボって pep 333 を読まないなら、要約はこれだ:
・WSGI アプリケーションは呼び出し可能な Python オブジェクト(関数か、2つの引数を取る __call__ メソッドを持つクラス:1 つが WSGI 環境とレスポンスを開始する関数)。
・アプリケーションは与えられた関数でレスポンスを開始して、渡される項目が書き出しとフラッシュを意図するイテレータ(iterable) を返さないといけない。
・WSGI 環境は CGI 環境みたいなもので、サーバやミドルウェアからいくつかのキーが追加されたものだ。
・アプリケーションをラップすることでミドルウェアを追加できる。

たくさん情報があるからとりあえずは無視して、基本的な WSGI アプリケーションを見ていこう:


* 拡張版 Hello World
これは単純だけど単純すぎるってことはない WSGI アプリケーションの例で、
Hello World! の World を url パラメタで指定して出力できる。


from cgi import parse_qs, escape

def hello_world(environ, start_response):
    parameters = parse_qs(environ.get('QUERY_STRING', ''))
    if 'subject' in parameters:
        subject = escape(parameters['subject'][0])
    else:
        subject = 'World'
    start_response('200 OK', [('Content-Type', 'text/html')])
    return ['''Hello %(subject)s
    Hello %(subject)s!
''' % {'subject': subject}]


見ると分かるけど、 start_response 関数は 2 つの引数を取っている。
ステータス文字列と、レスポンスヘッダを表すタプルのリストだ。
ここでもどこでも使われていないから分からないんだけど、 start_response 関数は
ある物を返す。これは write 関数を返して、ウェブサーバの出力ストリームに
直接書き込むことができるんだ。これはミドルウェア(これは後で触れるね)を
迂回してしまうので、この関数を使うのは非道いアイデアだけど、
デバッグ目的でなら便利に使える。

だけど、このアプリケーションはどうやって開始する?
誰もこの関数を呼んでないんだから、ウェブサーバはおろか
Python だってこれをどう扱っていいか分からない。
面倒だからここでは WSGI をサポートしたサーバの設定なんかしたくないけど、
Python 2.5 以降にバンドルされている wsgiref というWSGI の
スタンドアロンサーバを使うことができる。
(Python 2.3 か 2.4 用ならダウンロードもできるよ)

ファイルにこれを追加してみて:


if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    srv = make_server('localhost', 8080, hello_world)
    srv.serve_forever()


ファイルを実行すると http://localhost:8080/?subject=John では
Hello John! て出てくるはずだ。


* パスのディスパッチ
キミは、たぶん CGI とか PHP なら使ったことがあるよね。ならば、
大抵はユーザがアクセスできる複数のパブリックなファイル(.pl/.php) が
あって、それらで何かするということは知ってる。WSGI ではそうじゃない。
全てのパスを処理するただ一つのファイルがあるだけだ。だから、
さっきの例のサーバがまだ動いているなら、同じコンテンツを
http://localhost:8080/foo?subject=John からでも見られる。

アクセスされたパスは WSGI 環境の PATH_INFO 変数に、
アプリケーションの実際のパスは SCRIPT_NAME に保存される。
開発サーバの場合は SCRIPT_NAME は空かもしれないけど、
http://example.com/wiki に wiki がマウントされていれば
SCRIPT_NAME 変数は /wiki になるだろう。この情報で、幾つもの
独立したページをステキな URL で提供することができる。

この例ではたくさんの正規表現があって、現在のリクエストにマッチさせている:


import re
from cgi import escape

def index(environ, start_response):
    """この関数は "/" にマウントされて、hello world のページ
    へのリンクを表示する。"""
    start_response('200 OK', [('Content-Type', 'text/html')])
    return ['''Hello World Application
               This is the Hello World application:
continue
''']

def hello(environ, start_response):
    """上の例と同様だけど、URL で指定した名前を使う。"""
    # 存在すれば、url から名前を取得する。
    args = environ['myapp.url_args']
    if args:
        subject = escape(args[0])
    else:
        subject = 'World'
    start_response('200 OK', [('Content-Type', 'text/html')])
    return ['''Hello %(subject)s
            Hello %(subject)s!
''' % {'subject': subject}]

def not_found(environ, start_response):
    """URL が何もマッチしないときに呼ばれる。"""
    start_response('404 NOT FOUND', [('Content-Type', 'text/plain')])
    return ['Not Found']

# URL を関数に割り当てる。
urls = [
    (r'^$', index),
    (r'hello/?$', hello),
    (r'hello/(.+)$', hello)
]

def application(environ, start_response):
    """
    WSGI アプリケーションのメイン。上から順番に現在のリクエストを
    関数にディスパッチしてからその正規表現は 'myapp.url_args' として
    WSGI 環境に保存して、上の関数がこの url プレースホルダに
    アクセスできるようにする。
    何もマッチしなければ、'not_found' 関数が呼ばれる。
    """
    path = environ.get('PATH_INFO', '').lstrip('/')
    for regex, callback in urls:
        match = re.search(regex, path)
        if match is not None:
            environ['myapp.url_args'] = match.groups()
            return callback(environ, start_response)
    return not_found(environ, start_response)


けっこうあるね。だけど URL ディスパッチがどう動くか分かると思う。
基本的には、http://localhost:8080/hello/John にアクセスすると
よりカッコいい URL で上と同じ結果になって、間違った URL を入力すると
404 エラーのページになる。今度はこれをずっと改善して、
environ のリクエストオブジェクトへのカプセル化と、
start_response 呼び出しと返すイテレータのレスポンスオブジェクトへの
置き替えをしてみよう。これは werkzeug や paste みたいな
WSGI ライブラリもしていることだ。

環境に何かを追加することで、僕たちはミドルウェアが通常やっていることができる。
だから例外をキャッチして、それをブラウザに書き出すものを書いてみよう。


# トレースバックの取得とレンダリングに必要なヘルパー関数のインポート
from sys import exc_info
from traceback import format_tb

class ExceptionMiddleware(object):
    """使用するミドルウェア"""

    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        """例外をキャッチできるアプリケーションの呼び出し"""
        appiter = None
        # 単にアプリケーションを呼び出して、例外をキャッチする以外は
        # 出力を変更しないで戻す
        try:
            appiter = self.app(environ, start_response)
            for item in appiter:
                yield item
        # 例外が発生した場合、例外情報を取得してレンダリングできる
        # トレースバックの準備をする
        except:
            e_type, e_value, tb = exc_info()
            traceback = ['Traceback (most recent call last):']
            traceback += format_tb(tb)
            traceback.append('%s: %s' % (e_type.__name__, e_value))
            # ここまでで、レスポンスを宣言していないかもしれない。
            # ステータスコード 500 で応答してみて、もう応答済みならば
            # 発生する例外は無視する。
            try:
                start_response('500 INTERNAL SERVER ERROR', [
                               ('Content-Type', 'text/plain')])
            except:
                pass
            yield '\n'.join(traceback)

        # wsgi applications might have a close function. If it exists
        # it *must* be called.
        # wsgi アプリケーションには close 関数があるかもしれない。
        # ある場合は呼び出し *しないといけない* 。
        if hasattr(appiter, 'close'):
            appiter.close()


では、このミドルウェアはどうやって使うか?WSGI アプリケーションが前の例みたいに
application な場合、ラップすれば良い:


application = ExceptionMiddleware(application)


これで、発生する例外は全てキャッチされてブラウザに表示される。
もちろん、まさにこのことプラスより多くの機能をやってくれるライブラリは
たくさんあるからこれをする必要は無いんだけども。


* デプロイ
アプリケーションが "完成" したのでこれをどうにかして本番サーバに
インストールしなきゃいけない。もちろん mod_proxy の後ろで wsgiref を
使うこともできるけど、もっと洗練された方法だってあるんだ。多くの人は
WSGI アプリケーションを FastCGI 上で使う方法を好む。flup が
インストールされていれば、キミがやることは myapplication.fcgi を
定義するだけだ:


#!/usr/bin/python
from flup.server.fcgi import WSGIServer
from myapplication import application
WSGIServer(application).run()


apache の設定はこんな風になる:



    ServerName www.example.com
    Alias /public /path/to/the/static/files
    ScriptAlias / /path/to/myapplication.fcgi/



静的ファイル用の節もあることが分かる。もし開発途中、
WSGI アプリケーションの中で静的ファイルも提供したい場合には
いくつかのミドルウェアが利用可能だ。
(werkzeug、paste と Luke Arno のツールの "static" もこれを提供している)


* NIH / DRY
"Not Invented Here(自家製じゃない)"問題を避け、同じことを何べんも繰り返さないこと。
既存のライブラリとユーティリティを利用せよ!でも多すぎて!どれを使おう!オススメはあるよ。

** フレームワーク
Ruby on Rails が登場してからというもの、みんなフレームワークの話をしている。
Python にもメジャーなのが 2 つある。1 つはモロモロをすごい
抽象化した Django と呼ばれているもので、もう 1 つは WSGI にずっと近い
Pylong と呼ばれているものだ。Django はアプリケーションを配布しないならば
ヤバいフレームワーク。Web ページを一瞬で作る時。一方で Pylons は
デベロッパの作業を必要だけどデプロイはずっと簡単だ。

** ユーティリティライブラリ
多くの場合、全部入りのフレームワークは必要ない。それはキミのアプリケーションには
大きすぎるか、フレームワークで解決するには複雑すぎる。(フレームワークで
なにを解決してもいいんだけど、フレームワークの「助け」を借りないよりもずっと
ややこしくなり得るんだ)。

こんな時のためにいくつかのライブラリがある:
・paste -- Pylons の舞台裏で使われている。request と response オブジェクトを実装している。
たくさんのミドルウェア。
・werkzeug -- pocoo のために書いた最低限の WSGI ライブラリ。unicode 対応の request / response
オブジェクトと先進的な URL マッパ、インタラクティブデバッガを備える。
・Luke Arno's WSGI helpers -- Luke Arno による独立モジュールでの様々な WSGI ヘルパー。

** テンプレートエンジン
ボクがよく使い、推薦するテンプレートエンジンのリスト:
・Genshi -- 世界最高の XML テンプレートエンジン。でもちょっと遅くて、だから
本当に良いパフォーマンスが必要なら別なものにしないといけない。
・Mako -- バカ速いテキストベースのテンプレートエンジン。これは
ERB、Mason、Django テンプレートのミックスだ。
・Jinja -- 安全でデザイナー向けでかなり速い、テキストベースのテンプレートエンジン。
モチ個人的にはこれを選ぶ:D


* 結論
WSGI スゲー。簡単に個人的なスタックが作れる。
複雑すぎると思う場合は werkzeug や paste を見てみれば、
問題を制限なくずっと簡単にしてくれる。


この記事が役立つことを望みます。

0 件のコメント:

コメントを投稿