Python

Python2で文字列を処理する際の心掛け

More than 3 years have passed since last update.

日本語を扱う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

ユニコード文字列最高や!バイト文字列なんていらんかったんや!

とは、ならないのである。例として、以下のような簡単なプログラムを考えてみる。

test.py
#!/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

標準出力に渡す前にユニコード文字列をエンコードすれば、この問題は解決できる。

test.py(ユニコード文字列->バイト文字列)
#!/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