Python(1)WSGI:PythonからWSGIアプリでウェブブラウザに結果を出力する

2014-05-05

旧ブログ

t f B! P L

Pythonのスクリプトの中で得た結果をウェブブラウザに表示させるには、単純に結果をHTMLファイルに書き出して21.1. webbrowser — 便利なウェブブラウザコントローラーを使ってウェブブラウザを起動すればよいと思いましたが、WSGIを使っても同じようなことができそうなので使ってみます。

CGIはPython3.xで動く例の解説ページにめぐり合わず


Web Server Gateway Interface, PEP 333 または略して WSGI は現在のところ Python で web プログラミングをする最良の方法です。
Python を Web 上で使うには HOWTO — Python 3.3.3 ドキュメント

まず最初にこのページをみつけて読んだのですがちんぷんかんぷんでよくわからなかったので、他で見つけたCGIの例をいくつかやってみました。

私の環境はLibreOffice4.2バンドル版Python3.3.3で、IDEはPyCharm3.0.2です。

そもそもCGIはPHP言語で書くものと思っていてPythonで動くとは知りませんでした。

Python Tips - Programming

Pythonで簡易CGIサーバーを利用する - yummy-yummy

Pythonの紹介

このあたりを参考に四苦八苦したのですが、Python3.xへの対応の仕方がわからずブラウザからcgi-binフォルダを開こうとすると「Message: CGI script is not a plain file ('/cgi-bin/').」といわれますし、直接pyファイルをアドレスにいれると「サーバーが見つかりませんでした。」といわれてどうしてもダメでした。

まあ私がPython2.xが全くわからないというのが一番大きな原因でしょうけど。

某元祖 Advent Calendar 25日目 - PEP3333 - プログラマのネタ帳

で、ごにょごにょGoogle検索しているとこのページにWSGIについてPython3.xでの対応方法が書いてあるのを見つけました。

コードがばらばらに書いてあるので最初はうまく動かなかったのですが、全部まとめて最後にrun()で呼び出して実行するとうまく動きました。
from wsgiref import simple_server
def run():
    server = simple_server.make_server('', 8080, hello)
    server.serve_forever()
def hello(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return [b'Hello, world.']
run()
これをPyCharmで実行しておいてhttp://localhost:8080/にアクセスするとちゃんと「Hello, World.」と表示されています。


サーバーを停止するにはPyCharmのRun WindowのStopボタンをクリックします。

ということでWSGIについてちょっと学習してみます。

WSGIアプリの戻り値の日本語はstr.encode()でバイト列オブジェクトにして返す


「WSGI python3」でGoogle検索してみるとpython3/mod_wsgiで日本語表示 - PukiWikiが2番目にヒット。

早速読んでみるとreturnで日本語を入れるとエラーになると書いてあるではありませんか。

mod_wsgiというのはApache HTTP Serverのモジュール(mod_wsgi - Wikipedia)ということなので、Pythonの標準モジュールでは改善されているだろうと思って日本語をreturnしてみました。
from wsgiref import simple_server
def run():
    server = simple_server.make_server('', 8080, hello)
    server.serve_forever()
def hello(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return [b'こんにちは']
run()

実行する前からPyCharmにダメだしされています。

    return [b'こんにちは']
               ^
SyntaxError: bytes can only contain ASCII literal characters.

実行してみるとやっぱりエラーがでてきます。
    return ['こんにちは']
こうするとスクリプトを実行したときにはエラーはでませんが、http://localhost:8080/にアクセスすると、「サーバが見つかりませんでした」と言われて、PyCharmでは以下のエラーがでてきて動きません。

  File "C:\Program Files (x86)\LibreOffice 4\program\python-core-3.3.3\lib\wsgiref\simple_server.py", line 35, in close
    self.status.split(' ',1)[0], self.bytes_sent
AttributeError: 'NoneType' object has no attribute 'split'
    return [u'こんにちは']
これもダメです。

Python3.xではこのuリテラルは無視されますので意味はありませんので当然の結果ですけど。(byte列表現 Python 2 と Python 3 のユニコード文字列、バイト列の違いメモ - 銀月の符号

さあ、コマッタコマッタ、と思ってGoogle検索しても上記のmod_wsgiについてページ以外は問題の解決法もみつからないし、問題の発生に書いたページも見つかりません。

PythonのWebフレームワーク6種をかんたんに紹介 - モジログ

これを読んでBottleというフレームワークをいれてみようかな、と思ったのですがあちこち読んでみるとフレームワークというのはWSGIをインターフェイスに利用しているということがわかりました。

ということはフレームワークで日本語が返せると言うことはPythonの標準モジュールでも日本語が返せるはずです。

そこで気を取り直して上記のエラーででてきたC:\Program Files (x86)\LibreOffice 4\program\python-core-3.3.3\lib\wsgiref\simple_server.pyの35行目を見てみました。

ちょっとそこを見ても問題解決の糸口はつかめなかったのですが、simple_server.pyを最後までみてみると、デモ部分を見つけました。
def demo_app(environ,start_response):
    from io import StringIO
    stdout = StringIO()
    print("Hello world!", file=stdout)
    print(file=stdout)
    h = sorted(environ.items())
    for k,v in h:
        print(k,'=',repr(v), file=stdout)
    start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')])
    return [stdout.getvalue().encode("utf-8")]
returnする値にencode()メソッドを使っていますね。
文字列のエンコードされたバージョンをバイト列オブジェクトとして返します。標準のエンコーディングは 'utf-8' です。
str.encode(encoding="utf-8", errors="strict")
Python3.xではデフォルトでutf-8エンコーディングのはずですし、PyCharmでもUTF-8のエンコーディングを指定して書いているので関係ないと思ったのですがダメもとで戻り値の日本語をutf-8でエンコーディングしてみました。(というのは私の勘違いで、encode()メソッドはutf-8でエンコードされた文字列をバイト文字列に変換するメソッドでした。)
from wsgiref import simple_server
def run():
    server = simple_server.make_server('', 8080, hello)
    server.serve_forever()
def hello(environ, start_response):
    start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')])
    return ['こんにちは'.encode("utf-8")]
run()
なんとこれでうまくいくではありませんか。

ブラウザ側のエンコーディングがutf-8にならないときもあるので6行目でcharset=utf-8を指定しています。
    return ['こんにちは'.encode()]
str.encode()はデフォルトの引数が"utf-8"なのでこれでいけますね。

PEP 3333(Python Web Server Gateway Interface v1.0.1)を読む


WSGIサーバに渡す関数をWSGIアプリというようです。

WSGIアプリについては21.4. wsgiref — WSGI ユーティリティとリファレンス実装 — Python 3.3.3 ドキュメントではPEP 3333を読めと書いてあります。

とても長くて英語で読むのはしんどいと思ったら日本語訳をみつけました。

以下にWSGIアプリについて気になった部分を抜粋しました。
「バイト文字列」 (Python 3 では bytes 型、それ以外では str 型 を使用して実装される)、それはリクエストとレスポンスのボディ (例えば POST/PUT 入力データと HTML ページ出力) に使用される。

手短かに言えば: 本書で「文字列」という言葉を使った場合、それは「ネイティ ブ」の文字列、すなわち str 型のオブジェクトを指す。それが内部的には バイト列かユニコードとして実装されるか否かに関係なく。「バイト文字列」 に言及している場合、これは「Python 3 では bytes 型、 Python 2では str 型のオブジェクト」と読み替えること。

サーバに渡された、またはサーバから渡されたすべての文字列は、 Unicode オブジェクトではなく、 str または bytes 型でなければな らず、 unicode であってはならない。

この仕様で「バイト文字列」と呼ばれた値 (すなわち、 wsgi.input から 読まれた値や、アプリケーションによって write() または yield に渡さ れた値) については、その値は Python 3 では byte 型、それ以前のバー ジョンでは str 型でなければならない。

なぜこのインタフェースはこんなに低レベルなのか? 機能 X (Cookie, セッ ション、永続化、など) が欲しい!
これは Yet Another Python Web フレームワークではない。単にフレームワー クと Web サーバーが会話する方法である。これらの機能が欲しいなら、欲し い機能を提供する Web フレームワークを選ぶ必要がある。
PEP 3333: Python Web Server Gateway Interface v1.0.1 — knzm.readthedocs.org 2012-12-31 documentation

WSGIアプリ自体はバイト列のリストを返すだけのシンプルなものに限られるようです。

Pythonからウェブブラウザに結果を出力する


from wsgiref.simple_server import make_server
import webbrowser
def run():
    port = 8000  # サーバが受け付けるポート番号を設定。
    httpd = make_server("", port, app)  # appへの接続を受け付けるWSGIサーバを生成。
    url = "http://localhost:{}".format(port)  # 出力先のurlを取得。
    webbrowser.open_new_tab(url)  # デフォルトのブラウザでurlを開く。
    httpd.handle_request()  # リクエストを1回だけ受け付けたらサーバを終了させる。
def app(environ, start_response):  # WSGIアプリ。evironは必須WSGI変数を含む辞書。
    start_response("200 OK", [("Content-Type", "text/html; charset=utf-8")])  # start_responseは2つの必須固定引数statusとresponse_headers(タプル)、他にexc_infoを含む。
    html = '<a href="http://knzm.readthedocs.org/en/latest/pep-3333-ja.html">WSGIの仕様書の日本語訳</a>'  # 出力するhtml。
    return [html.encode()]  # htmlをバイト列に変換してWSGIサーバに渡す。
run()
これを実行するとデフォルトのブラウザの新しいタブに「WSGIの仕様書の日本語訳」が表示されます。

WSGIサーバはリクエスト1回受け付けたら終了します。

appへの接続を受け付けた状態でurlにアクセスしないといけないはずなので、7行目と8行目は順序が逆に思えますが、逆にしてみるとurlが開きません。

httpd.serve_forever()であっても同じです。不思議ですね。

ウェブページへのアクセスのコマンドを先に実行して、それからウェブサーバー起動のコマンドを実行しても、ちゃんとウェブページが開くのはウェブブラウザの動作がのろいおかげかもしれません。

参考にしたサイト


21.1. webbrowser — 便利なウェブブラウザコントローラー — Python 3.3.3 ドキュメント
Pythonプロセスからウェブブラウザを操作する。

Python を Web 上で使うには HOWTO — Python 3.3.3 ドキュメント
ひと通り知識を得てから読むといろいろ情報が得られます。

某元祖 Advent Calendar 25日目 - PEP3333 - プログラマのネタ帳
WSGIのPython3.xへの対応の例が紹介されています。

Python 2 から Python 3 への移植 — Python 3.3.3 ドキュメント
Python3.xからはuリテラルは無視されます。

Python 2 と Python 3 のユニコード文字列、バイト列の違いメモ - 銀月の符号
型の名称とリテラル表記にある比較表がわかりやすいです。

PythonのWebフレームワーク6種をかんたんに紹介 - モジログ
これを読んで使うならBottleがいいかなあ、と思いました。

4. 組み込み型 — Python 3.3.3 ドキュメント
 WSGIアプリケーションで日本語を戻り値にするにはstr.encode()が必要でした。

21.4. wsgiref — WSGI ユーティリティとリファレンス実装 — Python 3.3.3 ドキュメント
wsgiref.simple_server.make_server(host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler)を利用しました。

PEP 3333: Python Web Server Gateway Interface v1.0.1 — knzm.readthedocs.org 2012-12-31 documentation
WSGI仕様書の日本語訳。

次の関連記事:Python(2)WSGI:辞書environでウェブページからの情報を受け取る

ブログ検索 by Blogger

Translate

最近のコメント

Created by Calendar Gadget

QooQ