WSGIとPythonでスマートなWebアプリケーション開発を

第3回 WSGIミドルウェアの作成

この記事を読むのに必要な時間:およそ 3 分

何もしないミドルウェア

最初は,何もしないミドルウェアです。

リスト1 何もしないミドルウェアNopクラス

class Nop(object):
    ''' 何もしないミドルウェア (クラス版) '''
    
    def __init__(self, application):
        self.application = application


    def __call__(self, environ, start_response):
        return self.application(environ, start_response)

ミドルウェアの基本的な処理は,コンストラクタで呼び出すアプリケーションを受け取り,自身がアプリケーションとしてサーバ側から呼び出された時に,コンストラクタで受け取ったアプリケーションを呼び出す,というものです。

何もしないミドルウェアは,以下のように使用します。

リスト2 ミドルウェアの使い方

>>> application = Nop(application)

このようにしておくと, WebサーバからNopの__call__メソッドが呼び出され,その中でコンストラクタで渡したアプリケーションが呼び出されます。

URLマッピングを行うミドルウェア

次は,実際に機能するミドルウェアの例として,URLマッピングを行うミドルウェアを扱います。具体的には,アクセスされたURLごとにアプリケーションを割り当てるような処理を行います。

たとえば,WSGIアプリケーションにアクセスするための URLがhttp://somedomain.jp/であったとき,http://somedomain.jp/path1http://somedomain.jp/path2で別のアプリケーションに処理を割り振る,といったことができます。

リスト3 URLマッピングを行うミドルウェア

class SelectApp(object):
    ''' パスによるアプリケーション振り分けを行う '''

    def __init__(self, table, notfound=notFound):

        # パスは長い順にマッチさせたいので、あらかじめソートしておく
        tmp = sorted(table, key=lambda x:len(x), reverse=True)

        # 扱いやすいように、タプルのリストにしておく
        table = [(x, table[x]) for x in tmp]

        self.table = table

        # 割り振るパスが見つからなかったときに呼び出すアプリケーション
        self.notfound = notfound


    def __call__(self, environ, start_response):
        ''' リクエストのパスを見て振り分ける '''

        name = 'SCRIPT_NAME'
        info = 'PATH_INFO'

        scriptname = environ.get(name, '')
        pathinfo = environ.get(info, '')

        for p, app in self.table:

            if p == '' or p == '/' and pathinfo.startswith(p):
                return app(environ, start_response)

            # 同じパスならそのまま
            # 同じパスで始まっていて、その後にスラッシュがある
            if pathinfo == p or pathinfo.startswith(p) and \
                    pathinfo[len(p)] == '/':

                scriptname = scriptname + p
                pathinfo = pathinfo[len(p):]

                # リクエスト情報を書き換える
                environ[name] = scriptname
                environ[info] = pathinfo

                return app(environ, start_response)

        return self.notfound(environ, start_response)

このミドルウェアは,以下のように使用します。

リスト4 URLマッパの使い方

>>> application = SelectApp({'/path1':app1, '/path2':app2})

こうしておくと,/path1へのアクセスはapp1に,/path2へのアクセスはapp2に振り分けられるようになります。

それでは実装の解説に入ります。

SelectAppのコンストラクタでは,2つの引数を取っています。tableは,リスト4でコンストラクタに渡しているように,割り当てるURLをキーに,呼び出すアプリケーションを値に持つ辞書オブジェクトです。notfoundは,割り当てるURLが存在しなかったときに呼び出すアプリケーションです。デフォルトでは404を返すアプリケーションが使われます。割り当てテーブルは,キーであるURLの長い順にマッチさせるため,キーと値のタプルをキーが長い順に並べ替えたリストを生成して保持しておきます。

__call__ メソッドでは,リクエストされたURLごとにアプリケーションに処理を割り振ります。environ辞書の中で使用するのは,SCRIPT_NAMEとPATH_INFOです。

SCRIPT_NAMEはURL上でアプリケーションのルートがどの位置にあるかを表します。たとえば,http://localhost/app/pathにあるならば, SCRIPT_NAME は "/app/path" が入っています。

PATH_INFO は,アプリケーション上の仮想的な URL を表しています。http://localhost/app/pathがアプリケーションのある位置で,http://localhost/app/path/extraとアクセスした場合,PATH_INFOには"/extra"が入っています。

振り分けは,PATH_INFOとコンストラクタで受け取ったテーブルのパスとを1つずつマッチさせて調べます。その際に, Pythonの文字列型のメソッドstartswithを使用しています。これでpathinfoの文字列の先頭がpであるかどうかを調べています。

ただし,startswithだけでは'/path'の振り分けに'/path/path2'だけでなく'/pathother'なども含まれてしまいます。なので, PATH_INFO の文字列の長さがテーブルのパスよりも長い場合は, pathinfo[len(p)] が / になっているかどうかを調べています。

判定で一致した場合は, PATH_INFOの先頭からlen(p)分だけ文字を取り除き,SCRIPT_NAMEの末尾にpをそのまま加えて,environ辞書を更新したうえでアプリケーションを呼び出します。こうしておくことで,SelectAppを再帰的に適用して振り分けを行うことができます。

すべてのパスに一致しない場合は,notfoundアプリケーションを呼び出して,そのままreturnします。

まとめ

以上が WSGI アプリケーションにおけるミドルウェアの仕組みです。

このように,機能ごとに完全に独立したミドルウェアを作成し,それらを組み合わせて1つのアプリケーションとすることができるのがミドルウェアの利点です。

このようなミドルウェアは,すでにライブラリとして提供されているものもあります。代表的なミドルウェアライブラリにはpasteがあります。pasteには,今回作成したURLマッパと同機能のミドルウェアや, gzip圧縮をサポートするミドルウェアなどが存在します。これからWSGIアプリケーションを作成する方は利用してみてください。

今回までで,WSGIの基本的な仕組みの説明は終わりです。

次回は,実際にWSGIアプリケーションを動かすサーバとしてGoogle App Engine(以下GAE)を取り上げます。GAEはGoogleが提供するWebアプリケーションサーバです,GAEを利用すると,自分が作ったWebアプリケーションをGoogleのサーバ上で動かすことができます。作成したアプリケーションを非常に簡単にインターネット上で公開できるので,ぜひ試していただきたいサービスです。

著者プロフィール

保坂翔馬(ほさか しょうま)

CGプロダクションでプログラマとしてPloneで作成した社内情報共有ポータルの開発・管理,3Dモデリングソフトのツール・プラグイン開発,システム管理用補助ツール作成など幅広く仕事をしている。主たる業務はPloneポータルの開発。

仲間内ではプログラム言語オタクで通っており,好きな言語はPython, Haskell。

Twitter:@shomah4a