Hatena::ブログ(Diary)

思い立ったら書く日記

 | 

2009-09-21

Pythonでの日本語処理:Unicode型と文字列型

| 22:23 | Pythonでの日本語処理:Unicode型と文字列型を含むブックマーク

Pyhton の XML/HTML パーサ・ライブラリ BeautifulSoup を使って、Google の検索結果を整形する Python スクリプトを書いたところ、Python の日本語処理で UnicodeEncodeError、UnicodeDecodeError ではまった。いい機会なので、Python で日本語処理に関して、自分なりに整理してみる。

この記事は Windows での Python 2.5.1 で動作確認している。Python 3.x では改善しているかもしれないので、この記事を読む方はご注意を。Python 3.x については時間があれば確認したい。というより、早くバージョンアップしなさい!という感じですが。

[2009.09.22 追記]

Python 3.0 で Unicode まわりがかなり修正かかっていました。この記事を読む方は、Python 2.5.1 での日本語の取り扱いである、という点に留意してください。Python 3.0 以降だと、Unicode 型・文字列型ではなく、文字列型(= 今回の Unicode 型と同義かな)とバイト型となった模様。Python 3.0 以降にバージョンアップします。

サンプルコード

>>> a = u'ほげ'
>>> type(a)
<type 'unicode'>
>>> b = 'ふが'
>>> type(b)
<type 'str'>
>>> print a + b

Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    print a + b
UnicodeDecodeError: 'ascii' codec can't decode byte 0x82 in position 0: ordinal not in range(128)

ん、UnicodeDecodeError なるエラーが生じる。このエラーの原因はなんぞや?

Python における文字列

後述の参考情報や、その他インターネットの情報、自分で試した結果から、Python における文字列の取り扱いを以下の図にまとめてみた。

f:id:kaito834:20090921210108p:image:w400:h300

Python で文字列を扱う場合、Unicode型と文字列型の 2 つの型が存在する。

  • Unicode
    • 文字列をキャラクタとして扱う。'ほげ'という日本語文字列があった場合、len('ほげ')=2
  • 文字列型
    • 文字列をバイトとして扱う。'ほげ'という日本語文字列があった場合、len('ほげ')=4

Unicode 型はあくまで Python 内部の抽象概念であるため、Python から入出力する場合、入力元や出力先が分かる文字列型で文字列が扱われる。Python チュートリアルによると、Unicode 型は Python 2.0 から導入された概念であり、必ずしも Unicode 型を使用しなくてもよい模様。

日本語文字列のパターンマッチ等、日本語文字列をキャラクタとして扱う場合、Unicode型が適切と言える。また、インターネット上で公開されている Python パッケージでは、パッケージ内での文字列を Unicode 型として扱うものがある。このまとめを書くキッカケを作ってくれた BeautifulSoup も文字列を Unicode 型で扱う。Python で文字列を扱う場合、Unicode 型で扱う方が便がよいと言える。

Unicode 型と文字列型は相互に型変換が可能となっている。それぞれの型変換はエンコード、デコードという*1

この型変換は明示的に記述せずとも暗黙に実行される。この暗黙の型変換の場合、Unicodeエンコード・デコード時には、文字コードとして US-ASCII(7bit ASCII) が指定されるのだ。暗黙の文字変換に US-ASCII が使用されることが、UnicodeDecodeError、UnicodeEncodeError の原因となる。

Python 2.0 から、プログラマはテキスト・データを格納するための新しいデータ型、Unicode オブジェクトを利用できるようになりました。 Unicode オブジェクトを使うと、Unicode データ (http://www.unicode.org/ 参照) を記憶したり、操作したりできます。また、 Unicode オブジェクトは既存の文字列オブジェクトとよく統合していて、必要に応じた自動変換機能を提供しています。

(snip)

組込み関数 unicode() は、登録されているすべての Unicode codecs (COder: エンコーダ と DECoder デコーダ) へのアクセス機能を提供します。codecs が変換できるエンコーディングには、よく知られているものとして Latin-1, ASCII, UTF-8 および UTF-16 があります。後者の二つは可変長のエンコードで、各 Unicode 文字を 1 バイトまたはそれ以上のバイト列に保存します。デフォルトのエンコーディングは通常 ASCIIに設定されています。ASCIIでは 0 から 127 の範囲の文字だけを通過させ、それ以外の文字は受理せずエラーを出します。 Unicode 文字列を印字したり、ファイルに書き出したり、 str() で変換すると、デフォルトのエンコーディングを使った変換が行われます。

Python チュートリアル, 3.1.3 Unicode 文字列

サンプルコードの UnicodeDecodeError とは?

サンプルコードでは、Unicode 型変数 a、文字列型変数 b を + 演算子で連結した場合、暗黙的に文字列型は Unicode 型に型変換される。文字列型から Unicode 型に型変換される際、文字列型はデフォルトエンコーディング(US-ASCII)に基づいて、Unicode 型に変換(デコード)される。US-ASCII は 7bit までの値しか扱えないため、1 バイトの値が 128-255 となると、範囲外であるとのことで エラーが生じる*2。英数字を扱うだけなら問題ないのだが、日本語といったマルチバイトが Unicode 型、文字列型に混在すると、UnicodeDecodeError、UnicodeEncodeError に度々遭遇することになってしまう。

サンプルコードの解決方法

解決方法1: Unicode 型、文字列型を適切な文字コードで型変換する

Unicode 型、文字列型が混在する場合、明示的にどちらかに型変換を実施する。

### 文字列型 -> Unicode 型
# print で出力するのは、Unicode 型となる
#(厳密にはさらに型変換された文字列型)
print a + unicode(b, 'utf-8', 'ignore')

### Unicode 型 -> 文字列型
# print で出力するのは、文字列型となる
print a.encode('utf-8') + b

後述する解決方法2, 3 と比べると、Python コード単体できちんと解決するため、やはりこの解決方法が適切だろう。ただし、コード内部で常に Unicode 型、文字列型を意識する必要があるため、大規模プログラムを書く場合には現実解ではないかもしれない。この辺りは本職の方々にご意見がほしいところだ。主業務がプログラムを書くことではないので(^^;

解決方法2: Unicode オブジェクトのデフォルトエンコーディングを適切な文字コードに変更する

<Pythonインストールパス>\Lib\site-packages(Windows の場合)以下に、下記のコードを記述した sitecustomize.py を保存する。この sitecustomize.py により、暗黙の型変換時のエンコーディングを変更できるわけだ。

sitecustomize.py

import sys
sys.setdefaultencoding('utf-8')

sitecustomize.py を設定した後の Python Shell での実行確認

>>> import sys
>>> sys.getdefaultencoding()
'utf-8'
>>> a = u'ほげ'
>>> b = 'ふが'
>>> print a + b

Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    print a + b
UnicodeDecodeError: 'utf8' codec can't decode byte 0x82 in position 0: unexpected code byte

# Windows の Python Shell で実行したら、また見事に UnicodeDecodeError を
# 頂戴しました;; ただし、デフォルトエンコーディングが 'ascii' から
# 'utf-8' となっていることが分かる。

# sys.setdefaultencoding('shift-jis') として再度 Python Shell で試す 
>>> import sys
>>> sys.getdefaultencoding()
'shift-jis'
>>> a = u'ほげ'
>>> b = 'ふが'
>>> print a + b
&#130;〓&#130;°ふが
# Python Shell では結局文字化けしました。

Python では各 Python モジュールを読み込む際に、暗黙的に site モジュールを読み込む*3。この site モジュール内で、前述のsitecustomize.py が読み込まれる。Bash シェルにおける、.bashrc や .bash_profile といったイメージなんでしょう、きっと。本来であれば、sitecustomize.py ではなく、Python スクリプト内で sys.setdefaultencoding を呼び出せれば分かりやすくていいのですが、site モジュール内で sys.setdefaultencoding を del しているため、Python スクリプト内では setdefaultencoding を呼び出せない(なんてこった)。

個人で Python を利用するならこの解決方法でもいいように思うけど、コードを様々な環境下で利用することを想定した場合、環境依存である、この解決方法は問題となりそう。別の環境でいちいち sitecustomize.py を設定するのは面倒くさいし、他のコードの動作に影響を与える可能性もある。あと何より美しい解決策ではない(笑)

解決方法3:siteモジュールの読み込みをコントロールして、デフォルトエンコーディングを変更する

参考情報の 403 Forbidden で紹介していた方法。

サンプルコードを以下のように修正して、c.py として保存する。

import sys
sys.setdefaultencoding('utf-8')
import site

a = u'ほげ'
b = 'ふが'
c = a + b
print c

python に -S オプションに付与し、c.py を指定し実行する。

C:\>python -S c.py
ほげふが

C:\>python -h
usage: python [option] ... [-c cmd | -m mod | file | -] [arg] ...
Options and arguments (and corresponding environment variables):
-c cmd : program passed in as string (terminates option list)
(snip)
-S     : don't imply 'import site' on initialization
(snip)
PYTHONCASEOK : ignore case in 'import' statements (Windows).

暗黙的に読み込まれる stie モジュールを -S オプションで読み込み無効にして、Python スクリプト内で自分で読み込ませる。読み込ませる前に sys.setdefaultencoding() でデフォルトエンコーディングを変更する。Unix 系で Python を使うなら、この方法が一番いいのではないかと思う。ただ、Windows だと関連付けで -S オプションを設定しないと意味がないため、解決方法2 と同様に -S を設定したことを忘れそう。

まとめ

Python の日本語処理を自分なりに整理できた気がする。ちょっとすっきりした。

今後文字列を扱う場合は、基本的に Unicode 型で扱うのがいいのだろう。入力があったら、まず Unicode 型に変換して格納すれば、少なくとも + 演算子の連結処理で Unicode[En|De]codeError に遭遇する機会も少なくなり、暗黙的な型変換が生じにくくなる。で、Python 内部から出力する場合、Unicode 型から文字列型に型変換することを必須とする。・・・手間のかかる子というのは確か。

sys.setdefaultencoding() を site モジュールが del しなければいい気もするんだけど。

参考情報

【収集用メールアドレス】:q1w2e3w2@gmail.com

*1Python では内部的に Unicode を基本とするから、Unicode を中心に考え、標準となる Unicode から異なる型に変換することをエンコード、また標準となる Unicode に戻すことをデコードを呼ぶのかなーと妄想した

*2:この場合、UnicodeDecodeError が生じる

*3404 Not Found

トラックバック - http://d.hatena.ne.jp/kaito834/20090921/1253539430
 | 
Connection: close