JY-CONTENTS

JY

JY-CONTENTS
search

+

MENU

【Python】WSGI

【Python】WSGI

(DATE)

-

2017.07.23

(CATEGORY)

-

概 要

webページの表示を行います。
Pythonを用いてWebページの表示を行うためには、WSGIというインタフェース定義に従います。

PythonでWebアプリケーションを開発する際問題になっているものに、Webアプリケーションフレームワークを選択することによって、使用できるWebサーバが制限されてしまったりなどがあります。
Pythonで書かれたWebアプリケーションは、FastCGI, mod_python, CGI, さらにはWebサーバ独自のAPIを使ったものなど、様々な方法で実装されています。

この問題を解決するためにWSGIが考案さました。
WSGIは、Pythonにおける、WebアプリケーションとWebサーバを接続する標準仕様を定めるものであり、これによって、WSGIに対応したWebアプリケーション(やフレームワーク)は、WSGIに対応した任意のWebサーバ上で運用できるようになります。
アプリケーション側がWSGIに対応していれば、アプリケーションのコードに修正を加えることなく、WSGI対応サーバを自由に選択することができます。

WSGIには二つの側、サーバ側とアプリケーション側が存在する。
WSGIは、リクエスト情報・レスポンスヘッダ・レスポンス本文を、両者の間でどのようにやりとりするかをPythonのAPIとして定義している。

基本的な動き

簡単なwebアプリケーションを実行し、ブラウザに表示させてみます。

def hello(environ, start_response):
    start_response("200 OK", [("Content-type", "text/plain;charset=utf-8")])
    return [b"hello"]

「hello」を返すwebアプリケーションです。
WSGI で webアプリケーションを呼び出し可能なオブジェクトで定義してWSGI アプリケーションします。
引数を2つ(environ, start_response)受け取り、iterableオブジェクト(イテレータやリストなど)を返します。
2つ目の引数(start_response)を web アプリケーション内で呼び出します。

別説

hello 関数は,environとstart_responseという引数を持っています。

environにはWebサーバからPython組み込みの辞書型オブジェクトが渡されることになっています。辞書の中には,Webブラウザなどのクライアントから送られたリクエストの情報などが入っています。

start_responseは,2つの必須引数と1つのオプション引数を持った呼び出し可能オブジェクトが渡されることになっています。

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

start_responseに2つの引数を渡して呼び出しています。

第1引数の’200 OK’はHTTPのステータスコードです。リクエストを処理した結果に応じて’404 Not Found’などを返すようにしなければなりませんが,今回は簡単のために’200 OK’しか返していません。
第2引数はHTTPのレスポンスヘッダです。(ヘッダ名, 値)というタプルのリストを渡します。

return [b"hello"]

hello関数の返り値として,’hello’という文字列を返しています。
この返り値はHTTPレスポンスの本文として扱われます。
アプリケーションは,反復可能なオブジェクト(iterable object)を返さなければいけません。また,反復した結果として,文字列オブジェクトを返さなければいけません。

反復可能(iterable)なオブジェクトとは, 「for n in x:」というfor文の x の部分に使用できるオブジェクトで, __iter__ というメソッドを持っているものです。反復可能なオブジェクトには,リスト,タプル,辞書,文字列などがあります。また,ユーザ定義のクラスであっても, __iter__ メソッドを定義することで,iterableオブジェクトとして扱われます。

WSGIアプリケーションの仕様をまとめると

  1. 2つの引数を持った呼び出し可能なオブジェクト
  2. 第2引数として渡されたオブジェクトにHTTPステータスコードと[(レスポンスヘッダ, 値)]という値を渡して呼び出す
  3. 返り値にiterable objectを返す
  4. 返り値のiterableをiterateすると,文字列を返す

を満たすオブジェクトが, WSGI アプリケーションです。

呼び出し可能なオブジェクトとは,関数のようにx(arg1, arg2)などと,引数を渡して呼び出せるオブジェクトです。呼び出し可能なオブジェクトには,関数やクラスなどがあります。また,ユーザ定義のクラスでも, __call__(self, arg1, .. , argn) というようなメソッドを定義することで,呼び出し可能なオブジェクトになります。

from wsgiref.simple_server import make_server
def run(arg):
    server = make_server('', 8080, arg)
    server.serve_forever()
 
if __name__ == '__main__':
    run(hello)

WSGIアプリケーションを実行するためのサーバを動かす部分です。
make_server を使って実行してみます。
make_server 関数にはホスト、ポート、アプリケーションの3つの引数を渡します。

from wsgiref.simple_server import make_server
 
def hello(environ, start_response):
    start_response("200 OK", [("Content-type", "text/plain;charset=utf-8")])
    return [b"hello"]
 
def run(arg):
    server = make_server('', 8080, arg)
    server.serve_forever()
 
if __name__ == '__main__':
    run(hello)

これを python で実行後、http://localhost:8080/ へアクセスします。
「hello」が表示されます。

environ[‘QUERY_STRING’]

パラメータの値を受け取ってみます。
パラメータ environ の environ[‘QUERY_STRING’] には URL の ? 以降のクエリ文字列が入っています。

http://localhost:8080/?name=太郎 でアクセス

import urllib.parse
from wsgiref.simple_server import make_server
 
def greeting(environ, start_response):
    #query_string には 'name=太郎' が入る
    query_string = environ['QUERY_STRING']
 
    #params には {'name': ['太郎']} が入る
    params = urllib.parse.parse_qs(query_string)
 
    start_response("200 OK", [("Content-type", "text/plain;charset=utf-8")])
 
    #name には '太郎' が入る
    name = params.get('name', ['world'])[0]
    body = "Hello, {0}".format(name)
    return [body.encode('utf-8')]
 
 
def run(arg):
    server = make_server('', 8080, arg)
    server.serve_forever()
  
if __name__ == '__main__':
    run(greeting)

Hello, 太郎

params.get(‘name’, [‘world’])[0] の動き

params には

というふうdict型が入り、その値はリストです。
もしクエリが ?name=太郎&name=太郎2 であれば

というふうになります。

from urllib.parse import parse_qs
   
query = 'name=太郎&name=太郎2'
p = parse_qs(query)
 
print(p.get('name'))
print(p.get('name')[0])
print(p.get('name')[1])

[‘太郎’, ‘太郎2’]
太郎
太郎2

一連の流れ(動き)

Webサーバにリクエストが来ると、次のような流れでやりとりが行なわれます。

  1. サーバ側が、クライアントからリクエストを受ける。
  2. サーバ側は、アプリケーション側が提供するcallableオブジェクト(関数やクラスインスタンスなど __call__ が定義されたオブジェクト)を呼び出して、その引数として環境変数と1つのコールバック用callableオブジェクトを渡す。
  3. アプリケーション側は、このコールバック用callableオブジェクトを呼び出すことでステータスコードとレスポンスヘッダをサーバ側に伝え、さらに本文を生成するiterableオブジェクト(イテレータやリストなど)を戻り値として返す。
  4. サーバ側は、これらを用いてクライアントへのレスポンスを生成する。

WSGI ミドルウェア

  1. WSGIミドルウェアは、サーバ側とアプリケーション側のWSGIインタフェースを実装しているため、WSGIサーバとWSGIアプリケーションの”中間に”挿入できる。ミドルウェアはサーバーの視点からはアプリケーションとして振る舞い、アプリケーションの視点からはサーバーとして振る舞う。
  2. ミドルウェアとは,サーバとアプリケーションの両方のインターフェースを実装しているオブジェクトのことで,下記「ベーシック認証」では__call__関数がそれに該当していると言えます。
  3. 内部で別のWSGI アプリケーションを呼ぶWSGI アプリケーションをWSGI ミドルウェアといいます。
    ミドルウェアはWSGIアプリケーションがWSGIアプリケーションをラップします。
  4. ミドルウェアの基本的な処理は,コンストラクタで呼び出すアプリケーションを受け取り,自身がアプリケーションとしてサーバ側から呼び出された時に,コンストラクタで受け取ったアプリケーションを呼び出す,というものです。
  5. WSGIミドルウェアとは、
    サーバ側から見ると environ, start_response を受け取るアプリケーションとして振る舞う。
    アプリケーション側から見ると environ, start_response を渡して呼び出すサーバとして振る舞う。

何もしないWSGIミドルウェアの例

ただ受け取った関数に引数をそのまま渡すだけの高階関数

#===▼何もしないWSGIミドルウェア==========
def empty(app):
    def internal(environ, start_response):
        return app(environ, start_response)
 
    return internal
 
#===▼WSGIアプリケーション==========
def application(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/plain;charset=utf-8')])
    return [b'Hello WSGI world!']
 
#===▼サーバー==========
from wsgiref.simple_server import make_server
srv = make_server('', 8080, empty(application))
srv.serve_forever()

Hello WSGI world!

簡単なWSGIミドルウェアの例

アプリケーションの表示する文字を小文字に変換するWSGIミドルウェア

#===▼小文字に変換するWSGIミドルウェア==========
def lower_middleware(application):
    def func(env, start_response):
        res = application(env, start_response)
        return res.lower()
    return func
 
#===▼WSGIアプリケーション==========
def application(env, start_response):
    start_response('200 OK', [('Content-type', 'text/plain;charset=utf-8')])
    return [b'Hello WSGI world!']
 
 
#===▼サーバー==========
from wsgiref.simple_server import make_server
srv = make_server('', 8080, lower_middleware(application))
srv.serve_forever()

hello wsgi world!

ベーシック認証

hello というアプリケーションにベーシック認証のミドルウェアを適用します。

def hello(environ, start_respones):
    start_respones("200 OK", [("Content-type", "text/plain;charset=utf-8")])
    return [b"hello, world"]

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

import re
import base64
 
class BasicAuthMiddleware:
    def __init__(self, app, realm, authenticate):
        self.app = app
        self.realm = realm
        self.authenticate = authenticate
 
    #Webサーバからリクエストを受け取る
    #まずWebサーバにリクエストが届くと,__call__が呼ばれます。
    def __call__(self, environ, start_response):
        auth = environ.get('HTTP_AUTHORIZATION')
        if not auth:
            return self.unauthorized(environ, start_response)
 
        if not auth.startswith('Basic'):
            return self.unauthorized(environ, start_response)
 
        m = re.match(r'Basic\s+(?P<basic_auth>\w+)', auth)
        if m is None:
            return self.unauthorized(environ, start_response)
 
        basic_auth = m.groupdict()['basic_auth']
        basic_auth = base64.b64decode(basic_auth)
        basic_auth = basic_auth.decode('utf-8')
        user, password = basic_auth.split(':', 1)
        if not self.authenticate(user, password):
            return self.unauthorized(environ, start_response)
 
        return self.app(environ, start_response)
 
    def unauthorized(self, environ, start_response):
        start_response("401 Unauthoirzed",
                       [('Content-type', 'text/html'),
                        ('WWW-Authenticate',
                         'Basic realm="{realm}"'.format(realm=self.realm))])
 
        return [b'Unauthorized']

GET と POST

environ[‘REQUEST_METHOD’] の値で GET,POST どのメソッドでリクエストされたのかが確認できます。

#==▼メソッドの種類によって返す値を切り替えるWSGIアプリケーション=====
def calc(environ, start_response):
 
    #メソッドの種類を取得
    request_method = environ['REQUEST_METHOD']
 
    if request_method == 'GET':
        #GETの場合はformアプリケーションを返す
        return form(environ, start_response)
    elif request_method == 'POST':
        #POSTの場合は「post」と表示させる
        start_response("200 OK",[("Content-type", "text/html;charset=utf-8")])
        return [b'post']
 
 
form_body = """<html>
<form method="POST" action="?">
<input type="text" name="value" />
<input type="text" name="value" />
<input type="submit" value="Add" />
</form>
</html>
"""
 
#==▼フォームを表示させるアプリケーション=====
def form(environ, start_response):
    start_response("200 OK",
                   [("Content-type", "text/html;charset=utf-8")])
    #url = application_uri(environ)
    return [form_body.encode('utf-8')]
 
 
 
#===▼サーバー==========
from wsgiref.simple_server import make_server
srv = make_server('', 8080, calc)
srv.serve_forever()

GETでリクエストする

上記のようにGETでリクエストすると、environ[‘REQUEST_METHOD’] の値は「GET」になるのでサーバーにはformアプリケーションが返されるので画面には下記のようなフォームが表示されます。

フォーム

これはメソッドが「POST」に設定されているフォームです。
そして次にフォームに以下のように入力し、送信します。

フォーム

これを送信した場合はメソッドは「POST」になるので「post」という文字列が返され画面に表示されます。

NEW TOPICS

/ ニュー & アップデート

SEE ALSO

/ 似た記事を見る

JY CONTENTS UNIQUE BLOG

search-menu search-menu