はじめに
第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」の内容も変更になっています。
message.pyを実行し、http://localhost:8080/
にブラウザでアクセスすると、図1のようにメッセージを入力するフォームが表示されます。
フォームに名前、タイトル、本文を入力して「post」すると、書き込みフォームの上部に書き込まれたメッセージが図2のように新しい順に表示されます。
今回は、このようなメッセージボードアプリケーションを例として使用します。
ソースの内容
前回のHello, world.アプリケーションでは1つの関数を定義していましたが、今回は1つのクラスを定義しています。定義したクラスでは、__call__
というメソッドが定義されています。
特殊メソッド
__call__
のようにメソッド名の前後にアンダースコアが2つ付いているメソッドは、Pythonでは特殊メソッドと呼ばれます。
特殊メソッドを定義することで、ユーザ定義のクラスに組み込みオブジェクトのような振る舞いをさせることができます(特殊メソッドの詳細はPythonリファレンスマニュアルをご参照ください)。
__call__
が定義されたクラスのインスタンスは、リスト1のように関数と同様に呼び出すことができます。そのため、クラスのインスタンスであっても、WSGIアプリケーションとして使用可能です。
__call__
と同じように定義されている__init__
はクラスのコンストラクタで、インスタンス生成時に呼ばれます。
処理の流れ
メッセージボードの大まかな処理の内容です。
- __call__での処理
- Webサーバからリクエストを受け取り、リクエストメソッドによってlistMessagesかaddMessageに処理を振り分けます。
- GETリクエストの場合
- listMessagesを呼び出して今までに書き込まれたメッセージの一覧と入力フォームを出力します。入力フォームのデータは、同じURLにPOSTで送信され、
__call__
でaddMessageに振り分けられます。
- POSTリクエストの場合
- POSTされたデータからメッセージを生成し、messagesリストに追加します。追加した後は、同じURLにリダイレクトするようにレスポンスを返します。
それでは、リクエストが到着した時の処理の詳細とクラスの実装を見ていきます。
__call__
まずWebサーバにリクエストが届くと、__call__
が呼ばれます。
__call__
の定義は次のようになっています。
メソッドの引数にselfという引数が付いています。これは、__call__
の呼び出しに使われたインスタンスそのものです。Pythonでは、C++やRubyなどと違い、呼び出し元インスタンスを明示的に引数として取る必要があります。
__call__
の中では、environからREQUEST_METHODを取り出し、リクエストメソッドによって処理を振り分けています。
GETであればself.listMessagesを、POSTであればself.addMessageを呼び出し、それぞれの返り値をそのまま返しています。それ以外のリクエストメソッドの場合は処理を行わないため、501NotImplementedを返しています。
addMessage
続いて、POSTメッセージを受け取るaddMessageです。addMessageでは、POSTで送信されたデータを元にメッセージを生成し、保存します。
addMessageの定義は以下のようになっています。
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のソースです。
レスポンス本文の生成には、StringIOクラスを利用しています。StringIOとは、メモリ上のバッファに対してファイルオブジェクトのような入出力操作を提供するオブジェクトです。このStringIOオブジェクトに、レスポンスとして使用するHTMLをwriteメソッドを使用して書き出していきます。
まず、ユーザから投稿されたメッセージを出力しています。ユーザから投稿されたメッセージのデータは、全てxml.sax.saxutilsモジュールのescape関数を使用してエスケープした後に出力しています。これは、投稿されたメッセージ中に含まれる可能性があるHTMLタグを無効化するための処置です。この「出力時のエスケープ」を忘れてしまうと、クロスサイトスクリプティングなどの脆弱性につながるため、注意が必要です。
フォームのポスト先URLは、先ほども使用したrequest_uri関数を使用して、リクエストと同じURLに送るようにしています。
listMessagesの返り値には、先ほど使用したStringIOオブジェクトをそのまま使用しています。ファイルオブジェクトは反復可能なオブジェクトで、
と書いた場合、xにはfpから1行ずつ読み出されて代入されます。
ファイルオブジェクトと同様にStringIOもiterableです。なので、WSGIアプリケーションの返り値として利用可能です。ただし、write()で文字列を出力した後は、バッファ上の読み出し位置が変わってしまうため、seekメソッドで読み出し位置を先頭に設定した後で返り値として使用します。
まとめ
以上が、メッセージボードアプリケーションでの処理の流れです。メッセージボードアプリケーションを通じてWSGIアプリケーションにおいてのPOSTデータの受け取りや、動的ページ生成について理解していただけたのではないかと思います。
次回は、WSGIを利用する上で重要な「ミドルウェア」という概念についてとりあげます。ミドルウェアとは、サーバとアプリケーションの両方のインターフェースを実装しているオブジェクトのことで、今回のサンプルでは__call__関数がそれに該当していると言えます。ミドルウェアを利用すると、WSGIにおいてソースコードの再利用性がぐっと高まるため、ぜひ身につけてほしい概念です。