by shigemk2

当面は技術的なことしか書かない

Pythonのマルチバイト文字列出力のちょっとしたメモ

Emacsのshell-commandで以下のようなPythonワンライナーを実行するとUnicodeEncodeErrorが発生した。

(shell-command-to-string "/usr/local/bin/python2.7 -c \"import sys; sys.stdout.write(u'テスト')\"")
"Traceback (most recent call last):
  File \"<string>\", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-8: ordinal not in range(128)
"

(shell-command-to-string "/usr/local/bin/python3.7 -c \"import sys; sys.stdout.write(u'テスト')\"")
"テスト"

今回shell-command-to-stringを使ったけどshell-commandでもcall-process-shell-commandでも結果はいっしょだった。

でも、端末で同じコマンドを実行してもエラーは起きない(文字化けするけど)。

$ /usr/local/bin/python2.7 -c "import sys; sys.stdout.write(u'テスト')"
ãã¹ã
$ /usr/local/bin/python3.7 -c "import sys; sys.stdout.write(u'テスト')"
テスト

いろいろ調べていたら以下のような記載があった(公式ではない)

Python determines the encoding of stdout and stderr based on the value of the LC_CTYPE variable, but only if the stdout is a tty. So if I just output to the terminal, LC_CTYPE (or LC_ALL) define the encoding. However, when the output is piped to a file or to a different process, the encoding is not defined, and defaults to 7-bit ASCII.

はい。 Python2.7での標準出力の文字コードは sys.stdout.encoding で設定されており、基本はLC_CTYPE/LC_ALL/LANGなどの設定を見てsys.stdout.encodingの値が設定されているのだが、ttyじゃなくてパイプ/リダイレクトなど別プロセスを経由して結果を出力しようとした場合、文字コードは何も設定されない(3.7ではこの問題は解消されているようだが)

Encoding of Python stdout - Exterior Memory What a Character! | QED and NOM

試しに以下のように出力結果をリダイレクトして別ファイルに出力すると、結果が違う。

$ /usr/local/bin/python2.7 -c "import sys; print(sys.stdout.encoding)"
UTF-8
$ /usr/local/bin/python3.7 -c "import sys; print(sys.stdout.encoding)"
UTF-8

$ /usr/local/bin/python2.7 -c "import sys; print(sys.stdout.encoding)" > test.out; cat test.out
None
$ /usr/local/bin/python3.7 -c "import sys; print(sys.stdout.encoding)" > test.out; cat test.out
UTF-8

shell-command系関数を実行してシェルスクリプトを実行したときもttyじゃないから、sys.stdout.encodingがNoneになり、よってUnicodeEncodeErrorとなる。

なので、Python2.7での対策は、事前に PYTHONIOENCODING でstdin/stdout/stderrのエンコーディングを強制すること。

$ export PYTHONIOENCODING=UTF-8; /usr/local/bin/python2.7 -c "import sys; print(sys.stdout.encoding)" > test.out; cat test.out
UTF-8

旅をしすぎてタイトルをつけるのがめんどくさなった。