このサイトについて

みんなのPython Webアプリ編 - Webアプリケーションサーバを作る

みんなのPython Webアプリ編 - Webアプリケーションサーバを作る

Webアプリケーションサーバを作る

Pythonを使った一般的な開発と同じように、Webアプリケーションを開発する手法があれば、開発はより効率的になるはずです。ここで言う一般的な開発手法とは、1つの処理を1つごとのファイルに分けるのではなく、個別の処理は関数などとして実装して、関連する処理をモジュールのような形にまとめる、といった手法のことです。

これまで見てきたように、表面に見えるWebサーバの機能や働きは比較的単純です。たとえば、パスをフォルダやファイルの構造として解釈するのではなく、関数名として解釈するWebサーバも作れるはずです。URLを関数名として解釈して呼び出し、関数の返り値をレスポンスとして返します。このようなWebサーバができれば、個別の処理を関数として複数プログラムに定義できるようになり、いちいち個別のファイルに分けずに済むようになるはずです。

このように、Webアプリケーションの開発に特化したWebサーバのことをWebアプリケーションサーバと呼ぶことがあります。HTMLや画像などの静的なファイルをレスポンスとして返すためのWebサーバと区別する目的でWebアプリケーションサーバという言葉がよく使われます。

Webアプリケーションサーバの仕様を決める

Webアプリケーションサーバを作る前に、簡単に仕様を決めましょう。リクエストを受け取ったら関数を呼び出し、関数が返した結果をレスポンスとして返す、というのが最低限求められる要件です。

ただし、すべての関数をリクエストから呼び出せるのでは、セキュリティ上の問題が発生するかもしれません。内部利用を目的に作った関数まで不意に呼び出されてしまうかもしれないからです。そこで、Webアプリケーションサーバにあらかじめ登録した関数だけを呼び出せるようにしましょう。また、リクエストとして送られてくるクエリは、関数の引数として受け取れるようにしましょう。

リクエストを受け取り、関数に渡すには、GETとPOSTリクエストを処理しているハンドラメソッドを書き換えればよいはずです。メソッドの中でリクエストを解釈し、クエリを分割します。分割したクエリは、呼び出し用に登録された関数に引数として渡します。

Webアプリケーションサーバから呼び出された関数は、内部で必要な処理を行いレスポンスとして返します。関数は、レスポンス本文だけでなく、ヘッダを含む完全なレスポンスを返す必要があります。以前作ったテンプレートエンジン(SimpleTemplate)とResponseクラスを活用することにしましょう。

Webアプリケーションで画像やCSSなどの静的なファイルを扱いたいことがあるかもしれません。そのような場合に対応できるように、/static/~のように特定のパスから始まるリクエストを受けたときには静的なファイルをレスポンスとして返すようにしましょう。

クラス定義とリクエストを受け取るメソッド

Webアプリケーションサーバを作るに当たり、必要になるのはハンドラクラスを定義することです。ハンドラクラスのベースとなるクラスは標準モジュールに定義されています。今回は、標準モジュールのSimpleHTTPRequestHandlerクラスを継承して、必要最小限のメソッドだけを定義してみましょう。

新しく定義するハンドラクラスの名前をSimpleAppServerとします。このクラスの中で、リクエストを受け取るためのハンドラメソッドを定義します。GETリクエストを受け取るためにはdo_GET()メソッドを定義します。以下がクラス定義とdo_GET()メソッドの定義です。

do_GET()メソッド(simpleappserver.py)

:::python
class SimpleAppServer(SimpleHTTPServer.SimpleHTTPRequestHandler):

    static_dirs=['/static', ]

    def do_GET(self):
        """GETリクエストを処理する"""
        for sdir in self.static_dirs:
            if self.path.startswith(sdir):
                SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)  # (1)
                return
        i=self.path.rfind('?')
        if i>=0:
            path, query=self.path[:i], self.path[i+1:]
        else:
            path=self.path
            query=''
        self.handle_query(path, query)

static_dirsというアトリビュートには、画像やCSSのような静的なファイルを配信するためのパスをリストに登録してあります。リクエストを受けたときに、URLのパスがこのリストに登録されているものと一致するかどうかを調べて、処理を振り分けるために利用します。

do_GET()というメソッドがGETリクエストを受け取り、処理をするためのメソッドです。メソッドの初めでは、リクエストのパス(self.path)を調べて静的なファイルを配信すべきかどうかを判別しています。static_dirsアトリビュートに登録されている特別なパスから始まるURLについては、SimpleHTTPRequestHandlerのdo_GET()メソッドに処理を渡すことで静的なファイルをレスポンスとして返しています(1)。

リクエストが特別なパスから始まらない場合は、関数を呼び出します。GETリクエストではURL(パス)にクエリが記載されていることがあります。クエリがあったら抽出し、関数を呼び出すメソッドhandle_query()を呼び出します。

do_POST()メソッド

次に、POSTリクエストを処理するためのメソッドを見てみましょう。

POSTリクエストでは、リクエスト本文にクエリが記載されています。そのため、リクエスト本文を読み込んでクエリを解釈する必要があります。

do_POST()メソッド(simpleappserver.py)

:::python
    def do_POST(self):
        """POSTリクエストを処理する"""
        length=self.headers.getheader('content-length')
        try:
            nbytes=int(length)
        except (TypeError, ValueError):
            nbytes=0
        data=self.rfile.read(nbytes)
        self.handle_query(self.path, data)

まず、ヘッダからリクエスト本文の長さを取得しています。リクエスト本文は、self.rfileというファイル風のオブジェクトに格納されています。あらかじめ取得したリクエストの長さを使い、read()メソッドでリクエスト本文を読み込んでいます。クエリを読み込んだ後は、do_GET()と同じようにhandle_query()メソッドを読んで関数呼び出しを行います。

なお、このPOSTリクエストの処理では、静的なファイルに関する処理を考慮していません。

リクエストに合わせて関数を呼び出す

このWebアプリケーションサーバでは、リクエストに合わせて呼び出す関数を登録しておく、という仕様になっていました。ハンドラクラスを定義しているモジュールのトップレベルに、関数を記録するための辞書オブジェクトと登録用の関数を定義してあります。

以下がそのコードです。登録用の関数expose()では、引数として関数オブジェクトそのものを受け取ります。たとえば、foo()という関数を登録したい場合には、関数定義の直後でexpose(foo)というようなコードを書きます。もちろん、expose()関数はあらかじめインポートしておく必要があります。

expose()関数の内部では、引数で関数名の指定がない場合は、関数オブジェクトのfunc_nameアトリビュートを使って関数名を取り出し、関数登録用の辞書を更新しています。

expose()関数(simpleappserver.py)

:::python
# coding: utf-8

import BaseHTTPServer
import SimpleHTTPServer
import cgi
from httphandler import Response

funcs={}
def expose(func, func_name=''):
    """
    リクエストに反応して呼び出される関数を追加する
    """
    if not func_name:
        func_name=func.func_name
    if func_name=='index':
        func_name=''
    funcs.update({func_name:func})
    return func

do_GET()とdo_POST()では、リクエストに合わせて関数を呼び出すために別のメソッドを呼び出していました。クエリを受け取りメソッドを呼び出す処理は、GET、POSTとも共通しているため、別のメソッド、handle_query()に処理をまとめています。

以下がhandle_query()メソッドの定義です。引数として、パス、およびクエリ文字列を受け取ります。

まず簡単にhandle_query()メソッドの挙動を説明しましょう。expose()関数を使ってfoo()という関数を公開用に登録してあるとすると、~/fooというパスを持つリクエストをfoo()関数に受け渡すという動作をします。

クエリがある場合は、クエリを分解して引数として関数に渡します。クエリはPythonの辞書型と同じようにキーと値のペアで渡されます。これを、キーワード引数(引数名と値のペア)に変換して関数に渡します。また、〜/foo/bar/1のように関数名を示す文字列の後にパスか続いていたら、引数名を持たない引数として関数に渡します。これがhandle_query()の大まかな動きです。

handle_query()メソッド(simpleappserver.py)

:::python
    def handle_query(self, path, query):
        """
        クエリ付きのGET, POSTリクエストをハンドリングする
        """
        args=[]
        path=path[1:]
        if path.find('/') != -1:           # (1)
            args=path.split('/')[1:]
            path=path.split('/')[0]
        qdict=cgi.parse_qs(query, keep_blank_values=True)
        for k in qdict.keys():
            if isinstance(qdict[k], list) and len(qdict[k]):
                qdict[k]=unicode(qdict[k][0], 'utf-8', 'ignore')
            else:
                qdict[k]=unicode(qdict[k], 'utf-8', 'ignore')
        if path in funcs.keys():
            qdict.update({'_request':self})
            resp=funcs[path](*args, **qdict)       # (2)
            self.send_response(resp.status, resp.status_message)
            self.wfile.write(str(resp))
        else:
            self.send_error(404, "No such handler function (%r)" % path)   # (3)

メソッドの内部を見ていきましょう。冒頭部分で、まずパスを分割しています(1)。スラッシュで区切られた最初の部分のみを関数名として取り出し、もし複数の部分があれば関数に渡す引数リストとして保存しておきます。

その後は、cgiモジュールのparse_qs()関数を使ってクエリを解析しています。結果として返ってくる辞書オブジェクトを、関数へ引数として渡すためにローカル変数に保存します。

その後、expose()関数で管理しているfuncsオブジェクトを使い、リクエストに対応する関数を探し出します。関数は辞書の形式で格納されていますので、パスを元にキーがあるかどうかを判別します。キーの検査をしているifブロックの中に、特定のキーに相当する関数を呼び出すコードがあります(2)。funcspathという見慣れない書き方がしてありますが、この部分では辞書に登録されている関数オブジェクトに対して関数呼び出しを行っています。

関数呼び出しを行っている部分の引数でも、見慣れない書き方をしています。アスタリスクが1つ付いたargsというオブジェクトは、パスをスラッシュで分割したリストです。引数にアスタリスクを1つ付けリストを渡すと、リストの項目が分割されて引数に渡されます。また、アスタリスクが2つ付いたqdictはクエリを抽出した辞書です。辞書にアスタリスクを2つ付けると、辞書のキーと値がキーワード引数になって関数に引き渡されます。このようにして、パスやクエリを関数の引数として渡しています。なお、キーワード引数として渡す辞書には、ハンドラクラスのインスタンスメソッドを渡しています。このインスタンスメソッドには、リクエストのヘッダなどの有用な情報があるためです。

関数からは、Responseクラスのインスタンスオブジェクトが返ってきます。インスタンスオブジェクトを元に、ステータス行とレスポンス本文を送ります。

また、リクエストのパスに該当する関数が見つからなかったときは、ステータスとして404を返しエラーとして扱います(3)。

サーバの起動

最後に、Webアプリケーションサーバを起動するための関数を定義します。この関数をインポートして呼び出すと、Webアプリケーションサーバが起動し、Webブラウザでアクセスできるようになります。

今回作ったWebアプリケーションサーバを使うには、次のようなステップを踏みます。

1) リクエスト経由で呼ばれる関数を登録する

expose()関数を使って、Webアプリケーションサーバに関数を登録します。

2) Webアプリケーションサーバを起動する

Webアプリケーションサーバに起動用の関数test()を呼び出します。

起動用関数は、Webアプリケーションの関数などを定義したスクリプトファイルから呼び出すことになるでしょう。つまり、Webアプリケーションの関数と、起動用の関数を呼び出す部分がコードとして記載された「Webアプリケーション起動用スクリプト」ができ上がるわけです。

test()メソッド(simpleappserver.py)

:::python
def test(HandlerClass = SimpleAppServer,
         ServerClass = BaseHTTPServer.HTTPServer):
    SimpleHTTPServer.test(HandlerClass, ServerClass)

今回作るWebアプリケーションのコードはこれだけです。コードの総行数は70行強しかありません。Pythonの標準モジュールを使うと、初歩的な機能を持ったWebアプリケーションサーバがたった70行で書けてしまうのです。

WebアプリケーションサーバとCGIの違い

これまで作ってきたWebアプリケーションはCGIの仕組みを使って動いていました。Pythonのスクリプトファイルを設置し、スクリプトファイルを指すURLをリクエストとして送ると、Webサーバがスクリプトをプログラムとして起動します。この場合、スクリプトファイルはリクエストが送られるごとに、毎回起動することになります。スクリプトファイル上に定義したローカル変数などは毎回初期化されることになります。

WebアプリケーションサーバはCGIとは違った仕組みで動きます。Webサーバが起動するときに、リクエスト経由で呼び出す関数を登録します。関数は1つのファイルにまとめて書いてあっても構いません。また、Webサーバが起動している間は、関数などのオブジェクトがメモリ上に存在し続けます。

このように、CGIとWebアプリケーションサーバの間には、Webアプリケーションを動かす仕組みに大きな違いがあります。この違いを利用して、ちょっと面白い実験をしてみましょう。データベースやファイルのような仕組みを使わずに、アクセス回数を数えるカウンタを作ってみましょう。

以下がそのコードです。先ほど作ったWebアプリケーションサーバのハンドラクラス、Requestクラス、テンプレートエンジンSimpleTemplateを活用しています。これをcgi-binフォルダの中に保存します。

countertest.py

:::python
#!/usr/bin/env python
# coding: utf-8

from simpleappserver import expose, test
from httphandler import Response
from simpletemplate import SimpleTemplate

@expose               # (1)
def index(_request, d={'counter':0}):
    body="""<html><body><p>${counter}</p></body></html>"""
    res=Response()
    t=SimpleTemplate(body)
    body=t.render(d)
    d['counter']+=1
    res.set_body(body)
    return res

if __name__=='__main__':
    test()

index()という関数は、スラッシュで終わるインデックスアクセスを受け付けるための関数です。この関数の直前の行に、アットマーク(@)で始まる見慣れない表記があります(1)。これは関数デコレータと呼ばれています。この表記は、「expose()という関数に直後の関数オブジェクト(index)を渡して呼び出す」という内容の処理をします。ちょうど、「index=expose (index)」というコードと同じ意味になります。

expose()というのは、Webアプリケーションサーバのソースコードに定義された呼び出し関数を登録するための関数です(p.242)。この関数には関数オブジェクトを渡す約束になっていました。つまり、expose()関数を関数デコレータとして使うと、Webアプリケーション側に関数を登録するという処理が行えるわけです。

関数内の処理はとても簡単です。辞書のcounterというキーを表示するだけのテンプレートを定義し、レスポンスとして返しています。テンプレートエンジンに渡す辞書は、引数dに代入された辞書です。関数が呼び出されるたび、この辞書のcounterというキーに対応する値を1ずつ加算していきます。

Pythonでは、引数に定義された辞書やリストのような書き換え可能オブジェクトは、読み込み時に一度だけ初期化されます。今回作ったWebアプリケーションサーバでは、一度登録した関数などのオブジェクトはサーバが動いている間はずっとメモリ上に残り続けます。つまり、関数が呼び出されるごとに辞書のキーに相当する値が加算されていくわけです。テンプレートではキーの値を表示するようになっていますので、ブラウザをリロードするたびに値が加算されていくはずです。

countertest.pyの起動

CGIHTTPServerが起動している場合は終了させて、countertest.pyを起動してみてください。このファイルの最後にはWebアプリケーションサーバを起動するための関数が書き込まれていますので、スクリプトを走らせるとWebアプリケーションサーバが走り出します。スクリプトを起動したら、Webブラウザで「http://127.0.0.1:8000/」というURLにアクセスします。ブラウザをリロードするたびに、数値が1つずつ増えていくはずです。Webアプリケーションサーバを終了し、再度起動するとカウンタの数値が0に戻ります。再起動することによって辞書の内容が初期化されるためです。

CGIのように、毎回スクリプトが立ち上がるような仕組みを使う場合は、カウンタのような機能を実現するには、カウンタの値をファイルやデータベースに保存しておく必要があります。Webアプリケーションサーバでは、関数などのオブジェクトがメモリ上に長く残っているため、このようなトリックが利用できるのです。

2014-09-03 15:00