Python 2系で UTF-8 を取り扱う際の心得
Python 2系での日本語の扱い方について、忘れた頃に忘れてしまうのでメモ。
ソースで日本語(UTF-8)を使う
まずは基本から。
ソースの1行目または2行目にコメントを入れ、正規表現 coding[:=]\s*([-\w.]+) にマッチする文字列を書けばよい。
普通は以下のように書く。
# -*- coding: utf-8 -*-
shebang 付き (ここで Python のバージョンを指定したほうがよい場合もある)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
3つの文字列クラス
Python 2系の場合、今扱っているオブジェクトが何者かを意識することが大切。
- str
ASCII文字列向けに設計された文字列クラスだが、その中にはテキスト以外も格納できるので
実態はバイト配列のようなものとなっている。
>>> 'abc'
'abc'
>>> '\x03'
'\x03'
>>> 'あいう'
'\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86'
>>> type('abc')
<type 'str'>
- unicode
あらゆる文字コードのUnicode文字列を抽象化したクラス。
>>> u'abc'
u'abc'
>>> u'\x03'
u'\x03'
>>> u'あいう'
u'\u3042\u3044\u3046'
>>> print u'\u3042\u3044\u3046'
あいう
>>> type(u'abc')
<type 'unicode'>
- basestring
基底の抽象クラス。
文字列クラスかどうかを判定する場合に使用。
>>> isinstance('abc', str)
True
>>> isinstance(u'abc', str)
False
>>> isinstance('abc', basestring)
True
>>> isinstance(u'abc', basestring)
True
2つのメソッド
str 型から unicode 型への変換をデコード、その逆をエンコードと呼ぶ。
その変換処理は str クラスのメソッドとして定義されている。
これらのメソッドは、encoding と errors という 2つの引数を取る。
- encoding: 変換に使用する codec 名。
デフォルトは defaultencoding の値。
defaultencoding は sys.getdefaultencoding() で取得できるもので、そのデフォルトは 'ascii'。
- erros: エラーハンドラ名。デフォルトは 'strict' (変換に失敗したら例外を発生)
明示的にエンコードする
厄介なエンコードエラーが発生する主な要因は、暗黙的なエンコード・デコード処理と
エンコーディングのミスマッチにある。
>>> str(u'あいう')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-2: ordinal not in range(128)
これは前述の str.encode() メソッドにおける encoding が省略された状態で実行されたのと同じ現象が起きている。
この場合は utf-8 でデコードされた unicode オブジェクト(u'あいう')が、
defaultencoding である ascii でエンコードされたためにエラーとなってしまったのだ。
とはいえ実行時に defaultencoding を変更するのは、不可能ではないが特別な理由が無い限りすべきではない。
他のモジュールへもその影響が波及してしまうため、思わぬトラブルを引き起こすリスクがある。
解決策の一つは、以下のように明示的にエンコード・デコードを行うこと。そうすればエラーは起こらない。
>>> str(u'あいう'.encode('utf-8'))
'\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86'
"Zen of Python" の『曖昧さにあたっては推測をしたがるな』に従おう。
>>> import this
(略)
In the face of ambiguity, refuse the temptation to guess.
(略)
IO境界でラップする
Unicode の取り扱いに関するたった一つのベストプラクティスは、IO境界 --
つまりファイルの読み書きや、ネットワーク通信、画面出力などのタイミングで変換することだ。
- 入力のタイミングで、str から unicode にデコード
- 出力のタイミングで、unicode から str にエンコード
- 内部処理は全て unicode 文字列として取り扱う
Python 2.6 から利用可能になった io モジュールを使えばラッピングを簡便にしてくれる。
- ファイル入出力
>>> import io
>>> with io.open(FILEPATH, mode='w', encoding='utf-8') as file:
... file.write(u'あいう\n')
...
4L
- 標準入出力
標準出力を io.open() で開き直せば、適切なエンコーディングが選択される。
>>> import io, sys
>>> stdout = io.open(sys.stdout.fileno(), 'w')
>>> stdout.write(u'あいう\n')
あいう
4L
io.open() の場合、encoding パラメータを指定しなくても、環境に応じてよしなに
エンコーディングを判断してくれるのが素晴らしい。
実際には後述の locale.getpreferredencoding() の値が参照されているようだ。
Python クックブック(1.22章)には codecs.lookup を使った以下のような方法も掲載されていた。
>>> import codecs, sys
>>> old = sys.stdout
>>> sys.stdout = codecs.lookup('utf-8')[-1](sys.stdout)
環境に応じたエンコーディングを取得する
Python が実行された環境の標準的なエンコーディングを動的に取得するにはどうすればよいか。
完全な方法ではないものの、以下の2種類のアプローチを順に試せば大半の環境で意図した動作となるようだ。
- ファイルの encoding 属性
属性が存在しない場合もあるので、getattr() を使ったほうが安全。
- 例) Mac (utf-8)
>>> import sys
>>> getattr(sys.stdout, 'encoding', None)
'UTF-8'
- 例) Windows (SJIS)
>>> import sys
>>> getattr(sys.stdout, 'encoding', None)
'cp932'
- 例) Solaris (SJIS)
>>> import sys
>>> getattr(sys.stdout, 'encoding', None)
>>>
- locale.getpreferredencoding()
- 例) Mac (utf-8)
>>> import locale
>>> locale.getpreferredencoding()
'UTF-8'
- 例) Windows (SJIS)
>>> import locale
>>> locale.getpreferredencoding()
'cp932'
- 例) Solaris (SJIS)
>>> import locale
>>> locale.getpreferredencoding()
'PCK'
まとめ
- まず始めに、プログラムの設計段階で全てのIO境界を洗い出す
(図に書くのが一番よい)
- それぞれのIO境界において、どのエンコーディング(utf-8等)で
変換(decode, encode)を行うのか整理する
(同時に、環境に依存する部分や動的に取得する部分を明確にする)
- 明示的な変換(decode, encode)、明示的なエンコーディングの指定をする
(実行環境の defaultencoding (デフォルトはUS-ASCII) に依存しない実装をする)
(ライブラリ or 自作のラッパーを利用してもよい)
- それでも UnicodeDecodeError, UnicodeEncodeError が発生してしまったら、
変換前のデータ・データ型・変換しようとしたエンコーディングを再確認する
References