no-image

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

TL; DR

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

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

経緯

特に Python 2.x を使っているのに理由はなく、いい加減に Python 3.x に移行してしまおうと思い、これから書くプログラムはできるだけ Python 3.x にしようと思い立ちました(後になって、日本語周りが面倒になって投げたのを思い出した。歴史は繰返す)。

当初はプログラムそのままに実行環境だけを替えて、問題が出た部分を直していけばいいと軽い気持ちでいました。そしたらエラーどころか出力が一つも出ない、デバッグ用の cgitb の出力さえ出ない状況です。試しにターミナルで出力を確認すると、問題なく出力されています。

幸いにも Apache2 のログには残っていました。

UnicodeEncodeError: 'ascii' codec can't encode characters in position 7-10: ordinal not in range(128)

"UnicodeDecodeError" ではなく "UnicodeEncodeError" ですと……。ソースの該当場所を追ってみると print('日本語') のような何も問題なさそうな箇所です。

試しに print('Hello') に書き換えてみると問題なく動作します。これは間違いなくエンコード周りの面倒くさいやつですね。

確認用に次のような Hello, world の CGI を実行してみます。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

print('Content-type: text/html; charset=utf=8\n')
print('<p>Hello, world.</p>')

実行権限を与えてターミナルで実行したら問題なく出力されました。Web ブラウザから開いてみても問題なく表示されました。

次に "Hello, world." の部分を "こんにちは" に変更してみます。ターミナルでは問題なく出力されます。しかし、Web ブラウザでは途中で出力が止まるらしく、何も表示されません。ここで Apache2 の error.log を確認してみると、print の行で先ほどと同じ UnicodeEncodeError が投げられています。

Python 3.x は Python 2.x と文字列の扱いが異なるのは有名で、それに関する記事は探せばたくさん見つかります。皆さん同じところでハマるのでしょうか。今回もそれが原因だったわけですが、対処までに辿り着くのに非常に苦労したのでその記録を残そうと思った次第です。

原因探求

ここから先はスクリプト自体の文字コードがちゃんと UTF-8 になっていることが前提です。

Python 3.x では実行時の言語環境を環境変数で見ているようなので、ターミナル上と CGI 上で Python の使用している encoding を確認したのが以下になります。

ソース

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import os, sys, locale

print('Content-type: text/plain; charset=utf-8\n')

print('sys.getfilesystemencoding(): ' + sys.getfilesystemencoding())
print('locale.getpreferredencoding(): ' + locale.getpreferredencoding())
print('sys.stdout.encoding: ' + sys.stdout.encoding)
print('sys.getdefaultencoding(): ' + sys.getdefaultencoding())

ターミナルでの結果

Content-type: text/plain; charset=utf-8

sys.getfilesystemencoding(): utf-8
locale.getpreferredencoding(): UTF-8
sys.stdout.encoding: UTF-8
sys.getdefaultencoding(): utf-8

CGI での結果

sys.getfilesystemencoding(): ascii
locale.getpreferredencoding(): ANSI_X3.4-1968
sys.stdout.encoding: ANSI_X3.4-1968
sys.getdefaultencoding(): utf-8

上記結果から、sys.stdout.encoding が UTF-8 になってないのが原因というのはすぐわかります(ここに辿りつくまでかなり時間はかかってますが )。

試しにターミナルで次のように LANG を変更して同じコードを実行すると、結果が変るのがわかります。

$ LANG=C python3 lang.py
Content-type: text/plain; charset=utf-8

sys.getfilesystemencoding(): ascii
locale.getpreferredencoding(): ANSI_X3.4-1968
sys.stdout.encoding: ANSI_X3.4-1968
sys.getdefaultencoding(): utf-8

CGI 実行時の LANG はどうなっているか

次のようなコードを実行して CGI 実行時の環境変数を調べます。

ソース

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import os

print('Content-type: text/html; charset=utf-8\n')

for k, v in sorted(os.environ.items()):
    print('{}: {}'.format(k, v))

結果

LANG もなければ PYTHONENCODING  ありやしません。

SetEnv で LANG と PYTHONENCODING を設定した

Apache2 の設定に次の 2 行を加えました。

 SetEnv LANG ja_JP.UTF-8
 SetEnv PYTHONENCODING utf-8

再度確認してみましたが、環境変数は設定されているのに相変らず Python CGI は UTF-8 になっていません。CGI での実行時はまた別の話なのでしょうか。

対策

結局のところ、環境側で解決しても実行環境が変るとまた同じ目に遭うことが見えているので、ソース側で解決できないものかと調べてみました。どうやら stdout に TextIOWrapper でラップしてあげればよいようです。

import sys, io

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

これで日本語を print() しても問題なく出力されました。長かった……。

参考