読者です 読者をやめる 読者になる 読者になる

UnicodeDecodeError/UnicodeEncodeErrorに悩まないPython 2.x プログラミング

最近、ときどきTwitterで「Python」を検索して眺めていたのだが、Pythonの分かりにくいところとして「UnicodeDecodeErrorが出てうざい」という不満をよく見かけるようだ。
確かに、Pythonでは、数字やアルファベット以外のユニコード文字を使おうとすると、対応する処理を書かなければUnicodeEncodeErrorUnicodeDecodeErrorが出てしまう。Python3では色々改善されているのだが、Python2では分かりにくい点も多い。

このUnicodeDecodeErrorを見て、「Pythonは日本語が苦手だ」と考えてしまう人も多いだろう。確かにそう思ってしまっても仕方がないが、それは正しくない。日本人だけでなく、アメリカ人でもフランス人でもドイツ人でも、ユニコードを使う時はみんな等しく平等にこのエラーを出しているのである。

もちろん、慣れてしまえばPython 2.xでUnicodeEncodeErrorUnicodeDecodeErrorを出さないようにするのは難しいコトではない。以降で、Python2でユニコードを扱うコツを解説しよう。あまり厳密な用語や作法にこだわらず、できるだけ簡単で実用的な解説を心がけたいと思う。

基礎知識

簡単に基本を確認しておこう。

Pythonには、テキスト情報を扱うデータ型に2種類ある。ユニコード文字列のためのunicode型と、バイト文字列のためのstr型だ。

unicodeオブジェクトとstrオブジェクトは相互に変換することができる。unicodeオブジェクトからstrオブジェクトを作成することをエンコード(encode)」と言い、逆にstrオブジェクトからunicodeオブジェクトを作成することを「デコード(decode)」と言う。

strオブジェクトからunicodeにデコードするには、バイト文字列をユニコ−ドに変換するルールが分からなければならない。バイト文字列には色々な種類があり、主に日本語版Windowsで使われるShiftJIS、西ヨーロッパで使われるlatin1、最近ではUnix環境のデフォルトとなってきたutf-8など、世界各国でそれぞれ独自の方式を採用している。
Pythonでは、この様々なバイト文字列とユニコードを変換するためのルールをエンコーディング(encoding)」と呼び、エンコーディングに従って実際に変換を行うクラスのことを「コーデック(codec)」と呼ぶ。


ユニコード変換エラー

バイト文字列からユニコード文字列にデコードするとき、正しいエンコーディングを指定しないとUnicodeDecodeErrorエラーが発生する。例えば、

# -*- coding: ShiftJIS -*-
japanese = "日本語".decode("ascii")

のように、ShiftJISの日本語文字列を、asciiエンコーディングユニコードに変換することはできない。asciiコーデックは、当然日本語のことなど何も知らないからだ。ここでは、エンコーディングとしてShiftJISを指定しなければ、正しくユニコード文字列を作成することはできない。

逆に、ユニコード文字列からバイト文字列にエンコードする時も、エンコーディングの指定が正しくなければUnicodeEncodeErrorが発生する。

# -*- coding: ShiftJIS -*-
japanese = u"日本語".encode("ascii")

この例でも、日本語というユニコード文字列をascii文字列に変換することは当然できない。ShiftJISeuc-jputf-8などの、日本語をサポートしているエンコーディングを指定する必要がある。

ユニコード <-> バイト文字列の自動変換

明示的に処理を記述しなくても、勝手にPythonエンコード・デコードを行ってしまうケースがある。おそらく、この自動変換が「Pythonはやたらとユニコード変換エラーがでて使いにくい」という不満の原因だろう。自動変換は、デフォルトではasciiコーデックで行われる。従って、ユニコード文字列でもバイト文字列でも、日本語を含むデータが自動変換の対象となると、かならずエラーが発生してしまう。

バイト文字列->ユニコード文字列への自動的なデコードは、主に以下のようなケースで発生する。

文字列の演算
unicode_string + byte_string

のように、ユニコード文字列とバイト文字列の加算を行う場合などは、Pythonではまずバイト文字列をユニコード文字列にデコードしてから加算する。

このとき、バイト文字列をデコードするエンコーディングとして、デフォルトでasciiエンコーディングが使用される。上の例では、

unicode_string + byte_string.decode("ascii")

と同じ処理を行うことになる。このため、byte_stringに日本語が含まれていると、UnicodeDecodeErrorが発生してしまうのだ。

文字列の比較
unicode_string == byte_string
unicode_string > byte_string

のように、文字列同志の比較をする場合も、バイト文字列をユニコード文字列にデコードしてから比較する。従って、演算の場合と同様にUnicodeDecodeErrorが発生してしまうケースがある。ソースコード上では比較演算子を記述していない場合でも、

dictinary_obj[u'日本語']
set_obj.add(u'日本語')
sort(list_contains_japanese_bytes_and_unicode)

のように、辞書オブジェクトから値を取得する時など、陰でこっそり比較演算が発生してしまう場合がある。

ユニコード文字列->バイト文字列への自動的なエンコードは、主に以下のようなケースで発生する。

組み込み関数の呼び出し

ユニコード文字列をサポートしていない組み込み関数(Pythonの関数ではなく、C言語で作成された関数)に引数としてユニコード文字列を渡すと、そのユニコード文字列は自動的にバイト文字列にエンコードされる。代表的な例としては、ファイルのwrite()メソッドがある。

sys.stdout.write(u"日本語")

バイト文字列を使っている場合にはよく見かける処理だが、ユニコード文字列を使っている場合にはasciiコーデックによるエンコードが発生し、UnicodeEncodeErrorが発生してしまう。

print 文

print文を使ってユニコード文字列を標準出力・標準エラー出力に出力する場合、実行時のロケール設定に従ってバイト文字列に変換してから出力される。

#sys.stdout.write(u"日本語")はエラーだが、print文はエラーにならない
print u"日本語"   

現在のロケール設定で定義されていない文字が含まれている場合は、もちろんエラーとなる。例えば英語版Windowsprint u"日本語"とすれば、英語以外の文字は変換できないので、やはりUnicodeEncodeErrorが発生する。

また、この変換が行われるのは標準出力がTTYの場合のみである。Pythonの対話環境などで直接ターミナルに出力する場合にはロケール設定に従って自動変換が行われるが、パイプやファイル等にリダイレクトしている場合には、asciiコーデックでエンコードされてしまう。

$ python spam.py
日本語
$ python spam.py > spam.txt
Traceback (most recent call last):
  File "spam.py", line 2, in <module>
    print u"日本語"
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-2: ordial not in range(128)
$ python spam.py | less
Traceback (most recent call last):
  File "spam.py", line 2, in <module>
    print u"日本語"
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-2: ordial not in range(128)

私の記憶では、Pythonユニコードが取り入れられた当初はprint文でもロケール設定を参照せず、日本語をprintするとUnicodeEncodeErrorとなっていた。しかし、これでは対話環境であまりにも不便だ、ということでこの機能が追加された。

出力先によって動作が違うのは、出力がTTYならば人間が読むことが確実なので自動的にコーデックを選択しても良いが、リダイレクトされている場合は他のプログラムがその出力を読み込む可能性が高いので、プログラマがちゃんとエンコーディングを指定してインターフェース上の問題が出ないようにするべき、という理由だ。

ユニコード変換エラーを出さないために

では、ユニコード変換エラーを出さないためには、どうすればよいだろうか?結局のところ、変換をしなければ良いのである。具体的には (1)バイト文字列のみを使う、(2) ユニコード文字列のみを使う、のどちらかの戦略を取ることになる。

バイト文字列のみを使う

ユニコード文字列はできるだけ避け、バイト文字列のみで処理を行う戦略である。ファイル管理やシステム管理など、あまり日本語を意識する必要のない処理では、ユニコードなしでもそれほど不自由することはないだろう。ちょっとしたスクリプトなどでは、こちらがお勧めだ。

ただし、正規表現で日本語文字列を検索する場合などは、バイト文字列では正常に処理できない。

# -*- coding:utf-8 -*-

import re
line = raw_input()

# sre_constants.error: bad character rangeとなる
matched = re.search("[あーう]+", line).group()

このような場合は、一時的にユニコード文字列に変換し、その後すぐにバイト文字列に戻すようにする。

# -*- coding:utf-8 -*-

import re
line = raw_input()
u_matched = re.search(ur"[あーう]+", line.deocde("utf-8")).group()
matched = u_matched.encode("utf-8")

くれぐれも、ユニコード文字列をそのままにしてはならない。ユニコード文字列を作ったら、即座にバイト文字列に戻すのがコツだ。ユニコード文字列を残してしまうと、いずれどこかで自動変換が動いてUnicodeEncodeErrorになってしまうだろう。

また、「ユニコードなし」戦略の場合は、たまに出てきてしまうユニコード文字列に注意しよう。例えばPython標準のRDBモジュールsqlite3は、文字列データとしてユニコード文字列を返してくる。このようなモジュールを使用する場合には、受け取った文字列はその都度こまめにバイト文字列に変換するようにするか、ユニコード文字列のみを使う戦略に変更するべきだ。

ユニコード文字列のみを使う

逆に、Pythonでの処理は全てユニコードで行うパターンもある。バイト文字列は全てユニコードに変換してから処理を行い、外部に出力する直前にまたバイト文字列に戻して出力する。文字列定数も、全てユニコードリテラル(u"abcdefg")を使用する。

Python2では、基本的には外部からやってくる文字列の多くはバイト文字列だ。Pythonが動く現在のOSは、ほとんどバイト文字列を基本としてデータのやりとりを行うようになっており、Pythonは受け取ったバイト文字列をそのままスクリプトに渡してくることが多い。

バイト文字列を受け取ったら、何かをする前にまずユニコード文字列に変換し、それから処理を行うようにする。「必要な時に変換すれば良いや」などと中途半端なことを考えていると、かならず漏れが発生する。こまめにユニコード文字列に変換しよう。

ユニコード文字列をファイルに出力したり、ユニコード文字列をサポートしていないモジュールに渡したりする時には、必ずその直前にユニコード文字列をバイト文字列にエンコードするようにする。

ロケール情報

エンコード・デコードするとき、エンコーディングはどのように取得すればよいのだろうか?アプリケーションで決まった要件がなければ、基本的にはロケール設定に従うべきだ。locale.getpreferredencoding()で、ロケール設定から適切なエンコーディングを取得することができる。

# -*- coding:utf-8 -*-
import locale, sys
enc = locale.getpreferredencoding()

text = u"日本語\n"
sys.stdout.write(text.encode(enc))

また、ファイル名をエンコード・デコードするときには、sys.getfilesystemencoding()を使用する。

# -*- coding:utf-8 -*-
import sys, os

enc = sys.getfilesystemencoding()
curdir = os.getcwd().decode(enc)
filename = os.path.join(curdir, u"日本語")

with open(filename, "w") as f:
    f.write("hello\n")

それでも変換エラーが出るときは

ロケール設定に従って正しくエンコーディングを指定しても、変換エラーが出るときは出てしまう。コマンドライン引数や環境変数に本来使用できない文字を指定されてしまうかも知れないし、Webやメールなど、外部のサービスから取得したデータに外国語のデータが含まれている事も多い。

しかし、こういったケースで全てUnicodeEncodeErrorとしてしまうと、実用上問題があるケースも多い。例えば、

import sys, locale
encoding = locale.getpreferredencoding()

# 外部からデータを取得
data = getdata()

try:
    # 取得したデータを処理
    do_data(data)
except Exception:
    # エラー発生時、入力データとともにエラーメッセージを表示
    print >sys.stderr, "Invalid data:", data.encode(encoding)

のように、エラーメッセージの一部として入力データを表示する場合、ここでUnicodeEncodingErrorが出てしまっては、本来表示するべきだった重要なエラー情報を表示できなくなってしまう。エラーメッセージ以外でも、変換エラーとなる文字だけは省略して、一部分だけでも出力すれば良いというケースは多いはずだ。

このような場合、

import sys, locale
encoding = locale.getpreferredencoding()

data = getdata()

try:
    do_data(data)
except Exception:
    print >sys.stderr, "Invalid data:", data.encode(encoding, errors='replace')

と、変換時にerrors='replace'と指定すると、変換不能なときに例外を出さずに、変換不能文字を ? に変換することができる。このような変換失敗時の処理方法として、以下の5種類が用意されている。

'strict'
省略時のエラー処理で、UnicodeEncodeError/UnicodeDecodeErrorを送出する
'ignore'
例外を送出せず、単に不正な文字を無視する
'replace'
例外を送出せず、エンコード時ならu'\ufffd'に変換し、デコード時は?に変換する
'xmlcharrefreplace'
エンコード時のみ使用可能で、例外を送出せず、不正な文字をXMLの文字列参照(例:&#65533;)に変換する
'backslashreplace'
エンコード時のみ使用可能で、例外を送出せず、不正な文字をPythonの文字定数(例:\ufffd)に変換する

ユニコード文字列のファイル名

Pythonでファイル操作を行う場合、ファイル名やディレクトリ名にはユニコード文字列を使うべきだろうか?ユニコード文字列ファイル名のメリット・デメリットを考えてみよう。

Windowsの場合

Windows環境では、ファイル名はバイト文字列よりもユニコード文字列の方が有利だ。
現代のWindowsファイルシステムユニコードで構築されており、Pythonのファイル操作系関数はスクリプトからユニコード文字列を受け取ると、そのまま変換せずにOSに渡し、その結果のユニコード文字列をそのまま返すようになっている。このため、ファイル名に日本語以外の文字が入っていても、特に気にすることなく処理を行うことができるのだ。

また、日本語Windowsの標準エンコーディングはShiftJISだが、ShiftJISはファイル操作には不向きなエンコーディングである。ShiftJISではカタカナの”ソ"などの文字にはWindowsのパス区切り文字である "\" が含まれているため、そのままではglobなど、一部のファイル操作関数が誤動作してしまう。Windows用のスクリプトでは、できるだけユニコード文字列を使った方が良いだろう。

UNIXの場合

ユニコード文字列をファイル名として使用する場合、操作対象となるファイル名のエンコーディングが実行中のロケールと一致していれば良いが、そうでなければ問題が発生することがある。最近のLinuxなどでは日本語のエンコーディングとしてUTF-8を使用することが多いが、歴史的経緯のおかげでホームディレクトリ下にEUC-JPやShiftJISのファイル名をため込んでしまっている場合もあるのではないだろうか?

色々なエンコーディングによるファイル名が混在してしまっている場合、ロケール情報からファイル名のエンコーディングを決定することができないため、色々とめんどうなことになる。Python2.xでは、例えばos.listdir(u".")の結果がユニコード文字列とバイト文字列の混在となってしまうことがあるのだ。

Python2では、このようにファイル名のエンコーディングが不明な時にはバイト文字列を使用した方がよい。この辺の詳しい事情は、以前Python3.1の Unicode ファイル名にまとめたので、こちらを参照していただきたい。

また、実行時のロケールとファイル名のエンコーディングが食い違ってしまうケースがある。一般ユーザのロケールは正しくja_JP.UTF-8としていても、デーモンを起動するユーザではロケールCとなってしまっているような場合だ。このような運用が想定される場合も、ユニコード文字列は使用しない方が良いだろう。