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

第2回WSGIを使ったもう少し複雑なアプリケーションの作成

はじめに

第1回では「Hello, world.」を出力する簡単なアプリケーションを作成し、WSGIの仕様について説明しました。

しかし、⁠Hello, world.」のような簡単なアプリケーションからは、実際に作成するアプリケーションの雰囲気のを掴むのは難しいと思いますので、第2回では、もう少し実用的なアプリケーションを例にして、解説をしたいと思います。

MessageBoardアプリケーション

今回作成するアプリケーションは、MessageBoardアプリケーションです。フォームからタイトル、名前、本文を入力して投稿すると、それまでに投稿されたメッセージが一覧される掲示板のようなアプリケーションです。

アプリケーションのソースコードはこちらにZIP圧縮版があります。まずは、ダウンロードして以下のように実行してみてください。

■注意

サンプルソースmessage.pyで、マルチバイト文字を書き込んだ際に文字コードまわりでエラーが発生することがわかりました。これを修正したものに差し替えました。⁠2008/8/21)

具体的な修正箇所は、以下のとおりです。

  • listMessagesで文字をエスケープする際のUnicodeへの変換処理で、変換エラーを無視するようにした
  • listMessagesで出力するHTMLにmetaタグでcharsetを指定した
  • listMessagesの HTTPレスポンスヘッダのContent-type に charset=utf-8を追加した
  • listMessagesで出力しているformタグにAcceptEncoding="utf-8"を追加した

上記修正に伴い、記事中の「リスト4」の内容も変更になっています。

$ python message.py

message.pyを実行し、http://localhost:8080/にブラウザでアクセスすると、図1のようにメッセージを入力するフォームが表示されます。

図1 書き込み用フォームを表示したところ
図1 書き込み用フォームを表示したところ

フォームに名前、タイトル、本文を入力して「post」すると、書き込みフォームの上部に書き込まれたメッセージが図2のように新しい順に表示されます。

図2 書き込みをしたところ
図2 書き込みをしたところ

今回は、このようなメッセージボードアプリケーションを例として使用します。

ソースの内容

前回のHello, world.アプリケーションでは1つの関数を定義していましたが、今回は1つのクラスを定義しています。定義したクラスでは、__call__というメソッドが定義されています。

特殊メソッド

__call__のようにメソッド名の前後にアンダースコアが2つ付いているメソッドは、Pythonでは特殊メソッドと呼ばれます。

特殊メソッドを定義することで、ユーザ定義のクラスに組み込みオブジェクトのような振る舞いをさせることができます(特殊メソッドの詳細はPythonリファレンスマニュアルをご参照ください⁠⁠。

__call__が定義されたクラスのインスタンスは、リスト1のように関数と同様に呼び出すことができます。そのため、クラスのインスタンスであっても、WSGIアプリケーションとして使用可能です。

__call__と同じように定義されている__init__はクラスのコンストラクタで、インスタンス生成時に呼ばれます。

リスト1 __call__の呼び出し
>>> instance = someclass() # __call__ メソッドを持ったsomeclass クラスのインスタンスを作成
>>> instance(argument)     # instance.__call__(argument) と等価な処理

処理の流れ

メッセージボードの大まかな処理の内容です。

__call__での処理
Webサーバからリクエストを受け取り、リクエストメソッドによってlistMessagesかaddMessageに処理を振り分けます。
GETリクエストの場合
listMessagesを呼び出して今までに書き込まれたメッセージの一覧と入力フォームを出力します。入力フォームのデータは、同じURLにPOSTで送信され、__call__でaddMessageに振り分けられます。
POSTリクエストの場合
POSTされたデータからメッセージを生成し、messagesリストに追加します。追加した後は、同じURLにリダイレクトするようにレスポンスを返します。

それでは、リクエストが到着した時の処理の詳細とクラスの実装を見ていきます。

__call__

まずWebサーバにリクエストが届くと、__call__が呼ばれます。

__call__の定義は次のようになっています。

リスト2
def __call__(self, environ, start_response):
    ''' WSGI アプリケーション '''

    # リクエストメソッドを取得
    method = environ['REQUEST_METHOD']

    if method == 'GET':
        # GET の場合
        return self.listMessages(environ, start_response)

    elif method == 'POST':
        # POST の場合
        return self.addMessage(environ, start_response)

    else:
        # それ以外の場合は 501 を返す
        start_response('501 NotImplemented', [('Content-type', 'text/plain')])
        return 'Not Implemented'

メソッドの引数にselfという引数が付いています。これは、__call__の呼び出しに使われたインスタンスそのものです。Pythonでは、C++やRubyなどと違い、呼び出し元インスタンスを明示的に引数として取る必要があります。

__call__の中では、environからREQUEST_METHODを取り出し、リクエストメソッドによって処理を振り分けています。

GETであればself.listMessagesを、POSTであればself.addMessageを呼び出し、それぞれの返り値をそのまま返しています。それ以外のリクエストメソッドの場合は処理を行わないため、501NotImplementedを返しています。

addMessage

続いて、POSTメッセージを受け取るaddMessageです。addMessageでは、POSTで送信されたデータを元にメッセージを生成し、保存します。

addMessageの定義は以下のようになっています。

リスト3
def addMessage(self, environ, start_response):
    ''' メッセージを追加する '''

    # POST データを取得
    inpt = environ['wsgi.input']
    length = int(environ.get('CONTENT_LENGTH', 0))

    # 取得したデータをパースして辞書オブジェクトに変換
    query = dict(cgi.parse_qsl(inpt.read(length)))
    
    # POST メッセージを保存
    msg = {'name':query['name'],
           'title':query['title'],
           'body':query['body'],
           'date':datetime.datetime.now()}

    self.messages.append(msg)

    # リダイレクトを行う
    start_response('302 Found', [('Content-type', 'text/plain'),
                                 ('Location', util.request_uri(environ))])

    return ''

POSTされたデータは、environ変数のwsgi.inputから読み出します。wsgi.inputのreadメソッドに、environのCONTENT_LENGTHから取得したデータの長さを指定して呼び出し、その分だけ読み込むようにします。もし読み込みの際に読み出す長さを指定しないと、read関数の呼び出しが終わらず、実行がブロックされてしまうので注意が必要です。

POSTされたデータのパースにはcgiモジュールのparse_qsやparse_qslが利用可能です。今回は、parse_qslを使用して、[(名前, 値), ..] という形式に変換した後、そのまま辞書オブジェクトに変換し、利用しています。

保存するメッセージは、手軽さから辞書形式でそのまま保存しています。ただし、サンプルのアプリケーションでは、メモリ上にのみデータを保持するため、サーバを停止したり、リクエストの度にプロセスを立ち上げるような場合だとメッセージが保存されません。

最後に、クライアントへのレスポンスを返します。addMessageからlistMessagesを呼び出してそのまま表示を行う、ということも可能ですが、その場合、書き込んだ後にリロードすると二重書き込みが行われてしまいます。そのような事態を回避するために、今回は同一URLへのリダイレクトを行っています。リダイレクト先のURLの生成にはwsgiref.util.request_uri関数を使用しています。

request_uri関数はwsgiref.utilモジュールにあるユーティリティ関数です。この関数にWSGIサーバから渡されたenviron辞書を渡して呼び出すと、クライアントからリクエストされたURLを生成して返します。

listMessages

最後に、GETリクエストを処理するlistMessagesです。listMessagesでは、今までに投稿されたメッセージの表示と、投稿用フォームの表示を行っています。

以下がlistMessagesのソースです。

リスト4
def listMessages(self, environ, start_response):
    ''' 一覧表示 '''

    fp = StringIO.StringIO()

    # ヘッダを出力
    fp.write(r'''<html>
<head><title>Message Board</title>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
</head>
<body>
''')

    # メッセージ数分繰り返し
    for msg in reversed(self.messages):

        esc = saxutils.escape

        tmp = {}

        # 入力を全てエスケープして出力
        for key, value in msg.iteritems():
            value = str(value)
            tmp[key] = str(esc(unicode(value, 'utf-8', 'ignore')))


        # メッセージの内容を書き出す
        fp.write('''<dl>
<dt>title</dt>
<dd>%(title)s</dd>
<dt>name</dt>
<dd>%(name)s</dd>
<dt>date</dt>
<dd>%(date)s</dd>
<dt>message</dt>
<dd>%(body)s</dd>
</dl><hr />''' % tmp)

    # 書込み用フォームを出力
    fp.write('''<form action="%s" method="POST" AcceptEncoding="utf-8">
<dl>
<dt>name</dt>
<dd><input type="text" name="name"/></dd>
<dt>title</dt>
<dd><input type="text" name="title"/></dd>
<dt>body</dt>
<dd><textarea name="body"></textarea></dd>
</dl>
<input type="submit" name="save" value="Post" />
</form>
</body></html>''' % util.request_uri(environ))

    # シーク位置を先頭にしておく
    fp.seek(0)

    start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')])
    return fp

レスポンス本文の生成には、StringIOクラスを利用しています。StringIOとは、メモリ上のバッファに対してファイルオブジェクトのような入出力操作を提供するオブジェクトです。このStringIOオブジェクトに、レスポンスとして使用するHTMLをwriteメソッドを使用して書き出していきます。

まず、ユーザから投稿されたメッセージを出力しています。ユーザから投稿されたメッセージのデータは、全てxml.sax.saxutilsモジュールのescape関数を使用してエスケープした後に出力しています。これは、投稿されたメッセージ中に含まれる可能性があるHTMLタグを無効化するための処置です。この「出力時のエスケープ」を忘れてしまうと、クロスサイトスクリプティングなどの脆弱性につながるため、注意が必要です。

フォームのポスト先URLは、先ほども使用したrequest_uri関数を使用して、リクエストと同じURLに送るようにしています。

listMessagesの返り値には、先ほど使用したStringIOオブジェクトをそのまま使用しています。ファイルオブジェクトは反復可能なオブジェクトで、

for x in fp:
    ...

と書いた場合、xにはfpから1行ずつ読み出されて代入されます。

ファイルオブジェクトと同様にStringIOもiterableです。なので、WSGIアプリケーションの返り値として利用可能です。ただし、write()で文字列を出力した後は、バッファ上の読み出し位置が変わってしまうため、seekメソッドで読み出し位置を先頭に設定した後で返り値として使用します。

まとめ

以上が、メッセージボードアプリケーションでの処理の流れです。メッセージボードアプリケーションを通じてWSGIアプリケーションにおいてのPOSTデータの受け取りや、動的ページ生成について理解していただけたのではないかと思います。

次回は、WSGIを利用する上で重要な「ミドルウェア」という概念についてとりあげます。ミドルウェアとは、サーバとアプリケーションの両方のインターフェースを実装しているオブジェクトのことで、今回のサンプルでは__call__関数がそれに該当していると言えます。ミドルウェアを利用すると、WSGIにおいてソースコードの再利用性がぐっと高まるため、ぜひ身につけてほしい概念です。

おすすめ記事

記事・ニュース一覧