日本語を扱うPythonプログラマ(with Python2)にとっての最大の天敵(過言)、UnicodeEncodeError。
昨日横の人がその餌食となり、その解決の手助けをしているうちに自分の中でPython2での文字列処理の方向が少し整理できた。(近いうちにPython3バージョンもまとめたい)
個人的結論
- バイト文字列/ユニコード文字列のどちらを扱っているかを常に意識しておく。
- (基本的に)プログラム内ではユニコード文字列を扱い、標準入出力とやり取りする際(ex. print)にはバイト文字列に変換する。
バイト文字列とユニコード文字列
バイト文字列は特定のエンコード方式(ex. utf-8)でエンコードされており、リテラルでは'あいう'
のように表現する。一方、ユニコード文字列はUnicodeのコードポイントを並べたものであり、リテラルではu'あいう'
のようにu
をつける。
(py2.7)~ » ipython
(省略)
>>> 'あ' # バイト文字列
Out[1]: '\xe3\x81\x82'
>>> u'あ' # ユニコード文字列
Out[2]: u'\u3042'
>>> 'あ'.decode('utf-8') (or unicode('あ', 'utf-8')) # バイト文字列->ユニコード文字列(=デコード)
Out[3]: u'\u3042'
>>> u'あ'.encode('utf-8') # ユニコード文字列->バイト文字列(=エンコード)
Out[4]: '\xe3\x81\x82'
type
関数で確認すると、バイト文字列はstr
型/ユニコード文字列はunicode
型であることが分かる。
>>> type('a')
Out[5]: str
>>> type(u'a')
Out[6]: unicode
さらにPython2では、バイト文字列とユニコード文字列はどちらも文字列であり連結可能である。
>>> u'a' + 'a'
Out[7]: u'aa'
なんだ。何の問題もないじゃない。
そう、日本語(正確には非ASCII文字全般)を扱わなければね!
前述の例の出力を見ると分かるが、ユニコード文字列とバイト文字列を結合するとユニコード文字列が生成される。その過程でバイト文字列をユニコード文字列にデコードしなければならないが、ここで問題となるのがPythonの文字列は自身のエンコードに関する情報を何も持っていないということだ。
「エンコード方式が分からないならASCIIでデコードしてしまえばいいじゃない」とPythonは言い、UnicodeEncodeErrorがこんにちわする。さすがにリテラル同士でこのようなミスをするのは稀だろうが、自身のプログラム外(標準入出力含む)から受けとる文字列に関しては気をつけないとやってしまいがち。
>>> u'a' + 'あ' # ユニコード文字列とバイト文字列(非ASCII)の結合
---------------------------------------------------------------------------
UnicodeDecodeError Traceback (most recent call last)
<ipython-input-8-084e015bd795> in <module>()
----> 1 u'a' + 'あ'
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 0: ordinal not in range(128)
>>> u'a' + 'あ'.decode('utf-8') # バイト文字列->ユニコード文字列
Out[9]: u'a\u3042'
>>> print(u'a' + 'あ'.decode('utf-8'))
aあ
バイト文字列ではなくユニコード文字列に寄せるのは、文字列を操作する際にはバイトレベルよりもコードボイントレベルで処理する方が便利なことが多いためだ。例えば文字数をカウントしたい場合、ユニコード文字列ではlen
関数で良い。一方バイト文字列ではバイト数が返ってくるため、その意図では使えない。
>>> len(u'あいう')
Out[11]: 3
>>> len('あいう')
Out[12]: 9
ユニコード文字列最高や!バイト文字列なんていらんかったんや!
とは、ならないのである。例として、以下のような簡単なプログラムを考えてみる。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
print(u'あ' + u'いう')
ターミナルで実行してみる。おそらく大多数の人が問題なく実行できるだろう。
(py2.7)~ » python test.py
あいう
では、実行結果をファイルにリダイレクトするとどうか。以下のようにUnicodeEncodeErrorが発生する環境が多いのではないか。
(py2.7)~ » python test.py > test.txt
Traceback (most recent call last):
File "test.py", line 4, in <module>
print(u'あいう')
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-2: ordinal not in range(128)
ロケールとエンコード方式
例としたprint(u'あいう')
では標準出力にユニコード文字列を渡すが、この際、ユニコード文字列->バイト文字列の変換(エンコード)が行われる。
標準入出力がターミナルに接続してる場合はlocaleの値(ex. 環境変数LANG)から適切なエンコード方式をPythonが自動的に選択してくれる。一方、リダイレクトなどで標準入出力をターミナル以外に接続した場合、適切なエンコード方式を選択するための情報が得られず、ASCIIでエンコードを試み、大抵の場合(=非ASCII文字を含む場合)は失敗する。
(ref.) http://blog.livedoor.jp/dankogai/archives/51816624.html
標準出力に渡す前にユニコード文字列をエンコードすれば、この問題は解決できる。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
print((u'あ' + u'いう').encode('utf-8'))
と今まで思っていたが
PYTHONIOENCODING
という環境変数を指定することで、ロケールによらずに使用するエンコード方式を固定できるとのこと。これを指定すれば、いちいちエンコードしなくて良さそう。
(py2.7)~ » cat test.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
print(u'あ' + u'いう')
(py2.7)~ » PYTHONIOENCODING=utf-8 python test.py > test.txt
(py2.7)~ » cat test.txt
あいう
(ref.) http://methane.hatenablog.jp/entry/20120806/1344269400