カテゴリー: Python

  • Python WSGI で毎回 encode(‘utf-8’) するのが面倒

    Python WSGI で毎回 encode(‘utf-8’) するのが面倒

    Python の WSGI プログラムで出力をするたび

    yield '<h1>テスト</h1>'.encode('utf-8')
    yield '<p>あいうえお</p>'.encode('utf-8')

    と encode(‘utf-8’) を付けていましたが、どう考えてもこれは手間だし無駄な気がします。Flask を使えばいいと言われたらお終いなので、何か手はないかと考えてみました。

    Python にはデコレータ(decorator) という仕組があって、任意の関数の前後に好きな処理を追加することができます。関数も第一級オブジェクトである Python の特性をうまく利用した素敵機能ですね。

    yield を扱った decorator の例があまり見つからなくてやや苦戦しましたが、次のような decorator で実際に動作することを確認しました。

    def encode_utf8(func):
    	def wrapper(*args, **kwargs):
    		for r in func(*args, **kwargs):
    			yield r.encode('utf-8')
    	return wrapper

    前回の WSGI でフォームを扱うプログラムに適用させてみたのが次のコードです。

    # coding: utf-8
    
    from urllib.parse import parse_qsl
    
    def encode_utf8(func):
    	def wrapper(*args, **kwargs):
    		for r in func(*args, **kwargs):
    			yield r.encode('utf-8')
    	return wrapper
    
    @encode_utf8
    def application(env, res):
    	res('200 OK', [('Content-type', 'text/html; charset=utf-8')])
    
    	# Form
    	yield '<form method="get" accept-charset="UTF-8"><p><input type="text" name="get" value=""><input type="submit" value="GET"></form>'
    	yield '<form method="post" accept-charset="UTF-8"><p><input type="text" name="post" value=""><input type="submit" value="POST"></form>'
    
    	# Request method
    	method = env.get('REQUEST_METHOD')
    	yield '<p>Method: {}</p>'.format(method)
    
    	# Get query string
    	if method == 'POST':
    		wsgi_input = env['wsgi.input']
    		query = wsgi_input.read(int(env.get('CONTENT_LENGTH', 0))).decode('utf-8')
    	else:
    		query = env.get('QUERY_STRING', '')
    	yield '<p>Query: {}</p>'.format(query)
    
    	# Output form fields
    	form = parse_qsl(query)
    	for k, v in form:
    		yield '<p>{} = {}</p>'.format(k, v)

    行数は変らないのでぱっと見の変化少ないですが、書き忘れて Internal Server Error をもらう率は少し減りますね。

  • Python WSGI でフォームのデータを取得

    Python WSGI でフォームのデータを取得

    TL; DR

    POST

    query = env['wsgi.input'].read(int(env.get('CONTENT_LENGTH', 0))).decode('utf-8')

    GET

    query = env.get('QUERY_STRING', '')

    parse and split

    # dictionary
    form = urllib.parse.parse_qs(query)
    # list
    form = urllib.parse.parse_qsl(query)
    (さらに…)
  • Apache2 MPM-ITK で WSGI を daemon モードで動かす

    Apache2 MPM-ITK で WSGI を daemon モードで動かす

    Apache2 MPM-ITK 環境下の情報が少なすぎて解決するのに丸一日かかってしまいました。もうこんな目に遭うのはご免なので記録します。前回の記事「Python CGI プログラマのための WSGI 移行記録」の続きから書きます。環境は同じく Apache2 MPM-ITK on Debian 9 Jessie です。


    mod_wsgi には embedded mode と daemon mode があります。

    embedded mode は呼び出される度にプロセスを起動して実行するのでほぼ CGI と同じ実行方式です。プロセス ID をしばらく観察していると、すぐには終了せずにしばらく残存してから終了するみたいです。CGI は都度終了するのでそこが違いでしょうか。

    daemon mode は文字通り、デーモンとして起動したまま待機しています。プロセス ID を観察しているとずっと同じまま動作していることがわかります。

    Apache2 の設定に AddHandler wsgi-script だけを設定すると embedded mode で実行されます。daemon mode で実行するには設定を加える必要があります。

    Apache2 の VirtualHost に次の 2 行を加えます。

    <VirtualHost *.443>
        ...
        WSGIDaemonProcess wsgi_app socket-user=USERNAME
        WSGIProcessGroup wsgi_app
        ...
    </VirtualHost>

    保存したら Apache2 の再読込を行います。

    $ sudo systemctl reload apache2

    これで完了です。気になる方は次のような WSGI スクリプトを実行してみて、PID が間を置いたリロードで PID が変化するかどうかを試してみてください。

    # coding: utf-8
    
    import os
    
    def applicatoin(e, r):
        r('200 OK', [('Content-type', 'text/html; charset=utf-8')])
        yield '<p>PID: {}</p>'.format(os.getpid()).encode('utf-8')

    MPM-ITK 環境では socket-user オプションを指定しないと次のようなエラーを吐きます。

    503 Service Unavailable

    The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.

    このとき Apache2 の error ログには次のようなものが残ります。

    [Thu Oct 11 15:04:38.059487 2018] [wsgi:error] [pid 15574] (13)Permission denied: [client 153.194.19.27:55658] mod_wsgi (pid=15574): Unable to connect to WSGI daemon process 'wsgi_app' on '/var/run/wsgi.15557.0.1.sock' as user with uid=1001.

    これを見たら、パーミッションが不足していると思ってディレクトリのオーナーやパーミッションの変更ばかり試してハマりました。権限があるところもパーミッションが 777 のところを指定しても自体は解決しません。

    socket-user ではなく user と group オプションもありますがこれまた何も解決してくれず、おかげで丸一日悩む羽目になりました。

    以下はおまけ程度の内容です。

    実際に embedded mode と daemon mode でどれくらいの差があるのかを、Chrome の開発者モードに表示されるページの読込時間で試してみました。上記の PID を表示するだけの WSGI スクリプトをひたすら再読込するだけで、単位は ms です。

    embeddeddaemon
    初回100100
    直後3030
    間欠10070

    数回リロードして安定した数値を拾っているだけなのでかなり雑です。目安程度にしてください。直後というのは概ね数秒以内です。間欠は 10 秒以上間を空けています。

    サーバを起動した最初の 1 回目は同じですが、それ以後は daemon mode の方が有利ですね。開発時ならともかく、運用時はまず再起動することはないので daemon mode を選ばない理由はありませんね。

    複数ユーザへの対応

    MPM-ITK を選んでいるのは複数ユーザのパーミッション分離を行うためで、Apache2 もユーザ別に設定ファイルを書いています。仮に foo さんと bar さんと hoge さんがいるとすると、設定ファイルは以下のようにしておけば、それぞれで WSGI スクリプトが daemon mode で動きます。

    WSGIDaemonProcess 直後の名前は WSGIProcessGroup と揃えておけば好きな名前をつけて構いません。今回は WSGI であることがわかり、ユーザ別に分けたかったので wsgi_username の形にしました。

    foo.conf

    <VirtualHost *.443>
        ...
        ServerName foo.example.com
        ...
        WSGIDaemonProcess wsgi_foo socket-user=foo
        WSGIProcessGroup wsgi_foo
        ...
    </VirtualHost>

    bar.conf

    <VirtualHost *.443>
        ...
        ServerName bar.example.com
        ...
        WSGIDaemonProcess wsgi_bar socket-user=bar
        WSGIProcessGroup wsgi_bar
        ...
    </VirtualHost>

    hoge.conf

    <VirtualHost *.443>
        ...
        ServerName hoge.example.com
        ...
        WSGIDaemonProcess wsgi_hoge socket-user=hoge
        WSGIProcessGroup wsgi_hoge
        ...
    </VirtualHost>

    参考

  • Python CGI プログラマのための WSGI 移行記録

    Python CGI プログラマのための WSGI 移行記録

    Apache2 MPM-ITK on Debian 9 Jessie を想定しています。

    10 年近く書いてきた CGI を、昨今の WSGI に準拠したものへと移行するに当って大変苦労した記録です。技術的なことだけではなく、心情的なことも含んでいます。

    まず最初に書いてしまうと、CGI も WSGI も呼び出しと結果の受け渡しが違うだけで、プログラムの内容や動作には殆ど関係ありません(細かく言うと実行の仕方も違うはずですが、そこはどちらかというとサーバの都合だと思うので今回は考慮しません)。なので単に今まで書いた CGI を WSGI に移行するだけなら、あまりメリットは感じないどころか環境構築が煩雑過ぎて疲れるだけです。

    そもそもが CGI をどのような用途に使っているかによって、WSGI に移行すべきかどうかの判断が異なってきます。Raspberry pi やそれに近い組込み系の環境で単なる Web インターフェースとして使うのであれば、CGI の簡潔さは今なお有用です。ターミナル上で HTML の出力を確認しながらデバッグといったことも CGI なら簡単です。

    CGI の代替となる技術が登場した背景には、CGI の実行に「無駄」が多いとされたことがあります。CGI へのアクセスがある度に Web サーバはプロセス(インタープリタ等)を起動します。アクセス数が多くなるとこの手順はサーバにとって負荷になります。インタープリタは常に同じものを使うことが多いので、当然ながらインタープリタは常時起動しておけばいいのではないかという考えに至ります。Web サーバと Web アプリケーション間のやり取りには様々なインターフェースが開発されましたが、それを統一したものの一つが WSGI という規格です。Python という言語の中から生れた WSGI はその後、他の言語にも派生しました。

    さて、いよいよ Apache2 で WSGI を使って Python スクリプトを動かす準備に入ります。既に Apache2 で CGI が動いているなら実は変更することは 2 つです。

    • mod_wsgi のインストールと設定
    • AddHandler に wsgi-script を加える

    mod_wsgi は Debian 環境で apt 一発インストールです。このとき、Python 2.x を使うのか Python 3.x を使うのかによってパッケージが異なるので注意しましょう。今回は Python 3.x を使う予定なので Python 3.x に対応した “libapache2-mod-wsgi-py3” をインストールします。パッケージがあるか確認してインストールします。

    $ sudo apt search mod-wsgi
    Sorting... Done
    Full Text Search... Done
    libapache2-mod-wsgi/stable,now 4.5.11-1 amd64
     Python WSGI adapter module for Apache
    
    libapache2-mod-wsgi-py3/stable,now 4.5.11-1 amd64
     Python 3 WSGI adapter module for Apache
    
    $ sudo apt install libapache2-mod-wsgi-py3

    “sudo a2enmod wsgi” としなくても自動で有効化されていました。

    Apache2 の設定ファイル(*.conf)に加えるのは次の一行だけ。<Directory /var/www> </Directory> の中などに書き加えましょう。

    AddHandler wsgi-script .wsgi .py

    WSGI はインターフェースの名前であって、スクリプト自体は Python だから拡張子を変えるのは間違っている気もしますが、それを言ったらどんな言語でも拡張子を .cgi とするのもおかしな話になるので、気にしないでおきましょう。

    わかりにくいと思うので 000-default.conf を書き換えた例を示します。今回は実験用の独立したサーバなので全て許可した設定していますが、本番環境ではくれぐれも必要のないものまで許可しないようにしましょう。

    ...
    DocumentRoot /var/www
    <Directory /var/www>
        AllowOverride all
        Options ExecCGI
        AddHandler wsgi-script .wsgi .py
        Require all granted
    </Directory>
    ...

    設定を書き換えたら Apache2 を restart または reload します。

    $ sudo systemctl reload apache2

    または

    $ sudo systemctl restart apache2

    動作を確認するために簡単な WSGI スクリプトを書いてみます。基本の Hello, world. で。

    def application(environ, start_response):
        start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')])
        yield '<p>Hello, world!</p>'.encode('utf-8')

    これを /var/www/hello.wsgi として保存して http://[サーバのIPアドレス等]/hello.wsgi にアクセスして表示されたら完了です。CGI と違って chmod +x で実行権限を与える必要もありません。

    ついでに調べた情報についても記しておきます。

    def application() は呼び出し時にこの名前が呼ばれるので必須です。面倒だからと def app() にすると “404 Not Found” と怒られます。

    引数の environ, start_response は別名にしても問題ありません。つまり、引数名使ったキーワード呼び出しは使わず、引数の順番しか定義されていません。これは仕様書でもはっきり次のように書かれています。

    The application object must accept two positional arguments. For the sake of illustration, we have named them environ and start_response, but they are not required to have these names. A server or gateway must invoke the application object using positional (not keyword) arguments. (E.g. by calling result = application(environ, start_response) as shown above.)

    PEP 333 — Python Web Server Gateway Interface v1.0 | Python.org

    なので、例えば次のように書いても良いわけです。殆どが environ, start_response で書かれているようなので、そんなところで要らない個性を発揮する必要はないと思いますが、さっと動作確認したいときには省略したくなりますからね。

    def application(e, r):
        r('200 OK', [('Content-type', 'text/html; charset=utf-8')])
        yield '<p>Hello, world!</p>'.encode('utf-8')

    application() を呼び出したときの戻り値は Iteratable である必要があるので、例えば複数の出力をしたい場合は次の 2 通りのパターンで行えます。結果はどちらも同じです。普通に return バイト列 とやると 500 Internal Server Error でハマります。

    yield バイト列

    def application(environ, start_response):
        start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')])
        yield '<title>Hello, WSGI!</title>'.encode('utf-8')
        yield '<p>This is a WSGI sample.</p>'.encode('utf-8')

    return バイト列のリスト

    def application(environ, start_response):
        start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')])
        return ['<title>Hello, WSGI!</title>'.encode('utf-8'), <p>Hello, world!</p>'.encode('utf-8')]

    謝辞

    この記事を書くに当って、れお氏(@reoreo125)には多大なる貢献を頂戴したこと、誠に感謝申し上げます。

    参考

  • Python3 の CGI で日本語が出力できない!

    TL; DR

    print 文で UnicodeEncodeError が出ているなら次のコードで stdout をラップしましょう。

    import io, sys
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding = 'utf-8')

    (さらに…)

  • Python のテンプレートエンジン Jinja2 入門の入門

    CGI を書いていると出力する HTML の中で定型の部分が多くあることに気付きます。例えば <!DOCTYPE html> から </head> までは、どのページでもそう多く変化することはないと思います。

    CGI という仕組み上、最終的な出力は殆どが HTML です。そこに着目したのが PHP という言語で HTML の中にコードを埋めるという逆の発想で、これはこれで理に適っている仕組みです。静的なページに近い場合はこれでも良いかなと思いますが、ロジックを組むことが多いのでやっぱり Python で書きたい、でも保守性も考えたいとなったときに活躍するのがテンプレートエンジンです。

    pip が使えたら、とりあえずインストールします。

    $ pip install jinja2

    jinja2.Template

    最も基本的な使い方、単純に placeholder として使う場合です。

    from jinja2 import Template
    
    t = Template('こんにちは、{{ name }}さん')
    
    print(t.render(name = 'foo'))
    print(t.render(name = 'bar'))

    実行結果

    こんにちは、fooさん
    こんにちは、barさん

    {{ name }} のスペースをなくして {{name}} としても結果は同じですが、公式ドキュメントは全てスペースありで記述されているので統一します。

    jinja2.Environment, jinja2.FileSystemLoader

    HTML をプログラムから分離するのが今回のゴールなので、テンプレートは別ファイルとして用意します。ファイルからテンプレートを読み込むのに FileSystemLoader を使います。

    from jinja2 import Environment, FileSystemLoader
    
    env = Environment(loader = FileSystemLoader('./templates'))
    
    t = env.get_template('base.tpl')
    
    print(t.render(title = 'Sample Page', body = '<p>This is a Jinja2 sample page.</p>'))

    templates/base.tpl

    <title>{{ title }}</title>
    <body>{{ body }} </body>

    実行結果

    <title>Sample Page</title>
    <body><p>This is a Jinja2 sample page.</p></body>

    エスケープ処理

    鋭い方はもうお気づきだと思いますが、先の例では <p> タグがそのままテンプレートに埋め込まれています。これでは XSS の危険性があるので、エスケープ処理をする必要があります。Jinja2 には自動でエスケープ処理をしてくれる仕組みがあります。

    先程のソースの Environment を作成するときに autoescape を指定するだけです。

    from jinja2 import Environment, FileSystemLoader
    
    env = Environment(loader = FileSystemLoader('./templates'), autoescape = True)
    
    t = env.get_template('base.tpl')
    
    print(t.render(title = 'Sample Page', body = '<p>This is a Jinja2 sample page.</p>'))

    実行結果

    <title>Sample Page</title>
    <body>&lt;p&gt;This is a sample page.&lt;/p&gt;</body>

    select_autoescape

    select_autoescape() という、テンプレートの拡張子で autoescape をするかどうか決める仕組みもあります。import select_autoescape を加えて Environment の引数に autoescape = select_autoescape(('html.', 'xml')) とすれば、テンプレートの拡張が html か xml のときだけエスケープが働きます。

    テンプレート側で個別にエスケープ

    漏れが出たときに XSS のリスクとなるので避けたいところですが、テンプレート側で一つ一つエスケープする方法もあります。

    {{ body }}
    {{ body|escape }}
    {{ body|e }}
    <p>This is a sample page.</p>
    &lt;p&gt;This is a sample page.&lt;/p&gt;
    &lt;p&gt;This is a sample page.&lt;/p&gt;

    autoescape の部分的な有効化、無効化

    基本的に autoescape は有効にしておくべきですが、エスケープしてほしくない箇所があった場合は override して部分的に有効化および無効化できます。

    テンプレート側の {% autoescape true %}{% endautoescape %} で挟まれば部分は有効化され、逆に {% autoescape false %}{% endautoescape %} で挟まれた部分は無効化されます。

    テンプレートの書式

    ここまでは Python 側の話でしたが、ここからはテンプレート側の話です。

    {# … #} コメント

    Python の # と同じでコメントを記述できます。

    {% … %} 制御文

    for, if, macro/call, filter といった制御文を記述します。例えば for をつかって 1 から 5 までの連続した数値をリストアイテムとして出力する場合は次のようになります。

    <ul>
    {% for i in range(1, 6) %}
    {{ i }}
    {% endfor %}
    </ul>

    出力

    <ul>
    
    <li>1</li>
    
    <li>2</li>
    
    <li>3</li>
    
    <li>4</li>
    
    <li>5</li>
    
    </ul>

    元々 {% … %} があった部分は空行になって出力されます。空行の抑制は後述します。

    {{ … }} 値の出力

    変数の出力で使いますが、計算式を入れて計算結果を表示したりもできます。組込みのフィルタも多数用意されているため、値の加工も容易です。

    辞書から値を取出すのに . 区切り形式を使えます。Python で dict['key'] とするところを dict.key で表現できます。

    空白と空行の制御

    次のテンプレートと出力を基準として trim_blocks, lstrip_block, {%- … %} を指定したときの出力の違いを観察します。

    <ul>
        {% for i in range(1, 6) %}
        <li>{{ i ** 2 }}</li>
        {% endfor %}
    </ul>
    <ul>
    
        <li>1</li>
    
        <li>4</li>
    
        <li>9</li>
    
        <li>16</li>
    
        <li>25</li>
    
    </ul>

    trim_blocks = True, lstrip_blocks = False

    <ul>
            <li>1</li>
            <li>4</li>
            <li>9</li>
            <li>16</li>
            <li>25</li>
        </ul>

    trim_blocks = False, lstrip_blocks = True

    <ul>
    
        <li>1</li>
    
        <li>4</li>
    
        <li>9</li>
    
        <li>16</li>
    
        <li>25</li>
    
    </ul>

    trim_blocks = True, lstrip_blocks = True

    <ul>
        <li>1</li>
        <li>4</li>
        <li>9</li>
        <li>16</li>
        <li>25</li>
    </ul>

    {%- … %}

    trim_blocks = False, lstrip_blocks = False とした上で、ソースを下記のように変更。

    <ul>
        {%- for i in range(1, 6) %}
        <li>{{ i ** 2 }}</li>
        {%- endfor %}
    </ul>
    <ul>
        <li>1</li>
        <li>4</li>
        <li>9</li>
        <li>16</li>
        <li>25</li>
    </ul>

     参考

  • CGI でレスポンス最速はどの言語なのか

    まだしぶとく WSGI に移行せずに CGI を書いています。ちょっと気になったので、言語によって CGI の応答速度はどれくらい違うのか、調べてみました。

    環境

    • ConoHa VPS 512 MB
    • Debian 9 Stretch x64

    言語は次の通り。

    • C (gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1))
    • JavaScript (Node.js v10.10.0)
    • Python2 (Python 2.7.13 (default, Nov 24 2017, 17:33:09) [GCC 6.3.0 20170516] on linux2)
    • Python3  (Python 3.5.3)
    • Perl (v5.24.1)

    測定方法

    いずれの言語も次のような内容を出力するだけの最も簡単な CGI としました。

    Content-type: text/html; charset=utf-8
    
    <p>Hello, world! written in *.</p>
    
    

    応答時間は Chrome の開発者ツールで Network タブの表示を見ました。「リクエスト開始からレスポンスの最終バイトを受け取るまでにかかった合計時間」だそうです。

    結果

    言語時間 [ms]
    Perl28
    C29
    Python238
    Python346
    JavaScript95

    まとめ

    Perl が C と拮抗するとは意外でした。昔は CGI といえば Perl の時代でしたから、それだけ最適化が進んでいるのでしょうか。

    今回は単純に 2 行のテキスト出力を行っているだけですが、これがもっと複雑な処理を行うとなると結果は異るでしょう。参考程度に。