8.04.2013

Handling UTF-8 in Python 2.x

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'

 

まとめ

  1. まず始めに、プログラムの設計段階で全てのIO境界を洗い出す

    (図に書くのが一番よい)
     
  2. それぞれのIO境界において、どのエンコーディング(utf-8等)で
    変換(decode, encode)を行うのか整理する

    (同時に、環境に依存する部分や動的に取得する部分を明確にする)
     
  3. 明示的な変換(decode, encode)、明示的なエンコーディングの指定をする

    (実行環境の defaultencoding (デフォルトはUS-ASCII) に依存しない実装をする)
    (ライブラリ or 自作のラッパーを利用してもよい)
     
  4. それでも UnicodeDecodeError, UnicodeEncodeError が発生してしまったら、
    変換前のデータ・データ型・変換しようとしたエンコーディングを再確認する 

 

 

References

0 件のコメント:

コメントを投稿