Pythonの日本語環境まわりのノウハウ

最近、pythonの記事が増えてきたことは非常にうれしい。
そこで、自戒的な意味も含めてpythonで日本語の扱い方をまとめてみる。


間違ってたりしたらご指摘お願いします。ぺこりm(__)m
※ 実験の環境はWindows XP SP2 Python 2.4.4で行ってますが、ほかのOS環境でも同じと思う。


章構成はこんな感じで。

  1. python文字コード
  2. Pythonの3つのエンコード
  3. まとめ

Python文字コード

なにはともあれ文字コード

まず、日本語ならぬ文字コード処理をするためには、
USC,Unicode,UTF-8,UTF-16,EUC,Shift-JIS,JIS X 0208, JIS X 0213 などなどの違いを正しく理解していないといかんね。
(そもそも書いてるおまえが正しく認識してるのかよという話はおいといてw)

よく文字コードとかエンコード、character setは・・・とかいってるけど、
これは、Web記事から抜粋すると。。。。

  • 文字集合(文字セット)とは「含まれる文字を明示した集まり」です。例えば、常用漢字というのも文字集合の規格の一つですし、中国語や韓国語にはそれぞれの文字セット規格があります。
  • 符号化方式とはそれらの文字を「どのように数値コード(ビット組合せ)に対応させるか」というルールです。日本語の符号化方式は一般にJIS、シフトJISEUCの3つが用いられ、統一されていません。
  • さらに文字コードとは、JIS X 0202によれば「文字集合を定め、かつその集合内の文字とビット組合せを1対1に関係づける、あいまいでない規則の集合」ということになります(JISでは「符号化文字集合」と呼んでいます)。
http://www.kanzaki.com/docs/jcode.html#jiscode


とのことなので、ざっくりと以下の解釈でOKだと思う。

エンコード(符号化)
UTF-8、Shift-JIS、EUC,JIS(ISO-2022-JP)
キャラクターセット(文字集合)
Unicode、ISO-2022、JIS X 0208JIS X 0213


というわけで、一般に"文字コード"、"文字化け"といってるのは、1つめのエンコードのことさしてる。

(余談)
わかりやすくいうと同じエンコード(Shift-JIS)を利用しているWindows XPでも、
初期のバージョンはJIS X 0208JIS X 0213のアップデートを当てると文字が変わる。
これは同一のエンコードをしていても、キャラクターセットが異なるから。


あとはこの順で読めばおおよそ理解できると思う

  1. 日本語と文字コード
  2. 文字コードの話
  3. 備忘録: Unicode, UCS, and UTF
  4. Unicodeは文字集合か符号化方式か

知識の蛇足的にはこのあたりも必要。

Pythonエンコーディング

pythonでascii以外の文字を取り扱うときには次の3つを意識する必要がある。(まあpythonだけじゃないけどね)

  1. ソースコードエンコーディング
  2. 入出力する端末・環境のエンコーディング
  3. unicode()やprint等の相互変換時のデフォルトエンコーディング
1.ソースコードエンコーディング

まず、1.のソースコードエンコーディング(エンコード宣言)は、
Pythonソースコードでよく見るこれ。

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

これは、<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />と同じ役割です。*1
つまり、「このプログラムがutf-8で記述されてますよ」っていう意味。
なので、sjisなら # -*-coding: sjis -*- となります。
ちなみに、このエンドコード宣言は以下の3つを満たせばpythonは解釈してくれます*2

  • 1行か2行目に書かれている
  • 正規表現 coding[=:]\s*([-\w.]+) にマッチする
  • その文字集合をサポートしている。


最後の文字集合をサポートしている”というのは標準エンコーディングでサポートされているということ。
つまりこれはPAYTHONPATH\Lib\encodings以下に文字コードが存在するかということ。

(余談)
標準エンコーディングを見ればわかると思うけど、
Windowsでよく使われるでShift-JISは、Pythonではsjisとcp932 は違うので注意。
cp932(ms-kanji)を使うのが無難。


これを確かめるために以下のコードを実行して実験。

%% ファイルの中身
C:\Python24>type enc.py
# coding=shift_jis
print "helloハロー"
print u"helloハロー"

%% Lib\encodings\shift_jisがある場合
C:\Python24>python enc.py
helloハロー
helloハロー

%% SHIFT_JISと大文字でもOK. Linux版でもOK
%% ファイル中身
C:\Python24>type enc.py
# coding=SHIFT_JIS
print "helloハロー"
print u"helloハロー"

C:\Python24>python enc.py
helloハロー
helloハロー

%% Lib\encodings\sjift_jisがない場合
%% shift_jis.pyをリネームして、キャッシュ(shift_jis.pyc)を消す
C:\Python24>rename Lib\encodings\shift_jis.py shift_jis_bak.py   
C:\Python24>del lib\encodings\shift_jis.pyc                   
C:\Python24>python enc.py
  File "enc.py", line 1
SyntaxError: encoding problem: with BOM

大丈夫。想定内だ。
というわけで見事に失敗で実験成功。


2.入出力する場合のエンコーディング

次に2の入出力するのエンコーディング
これは、入出力する場所・環境によってエンコーディングは変わる
htmlならmetaに記載されているエンコーディング
windowsコマンドプロンプトならcp932、
Linuxならコンソールのロケール
OS Xではutf-8 が標準かな?
ファイルならばそのファイルのエンコード


そこで、以下の実験をして確かめる。
次のコードをエディタでshift-jisで保存(今回はenc.pyのファイル名で保存)。
(ファイルの中身)

# coding=shift_jis
import sys
str = "helloハロー"
uni = u"helloハロー"
print str
print repr(str)
print uni
print repr(uni)


(実行結果)

C:\Python24>python enc.py
helloハロー
'hello\x83n\x83\x8d\x81['
helloハロー
u'hello\u30cf\u30ed\u30fc'


同じ"helloハロー"結果でも、その中身をバイト列の中身を表示してやると結果が違う。
大丈夫。想定内だ。


これは、Pythonでは内部の文字列は次の2つの形式のどちらかで保持しているから

String
従来からある文字列。中身はバイト列
Unicode String
python2.0(1.6)からサポートされたユニコード文字列。中身はUCS2/UCS4で保存(pythonコンパイル時のオプションで決まる)


strの「helloハロー」はバイト列、uniの「helloハロー」はunicode


ここでキモとなるのが、unicode列は"UCS2"で保存されているということ、
決して"UTF-8"で保存されていない。
(もちろんutf-8バイトコードエンコードされてるけど。これは最後の余談を参照)

なお、自分のPythonがどのUSCなのかは
import sys; sys.maxunicodeで確認できる。
USC2ならば65535、USC4なら1114111

これをUTF-8で保存して再度実行。
(実行結果)

C:\Python24>python enc.py
hello繝上Ο繝シ
'hello\xe3\x83\x8f\xe3\x83\xad\xe3\x83\xbc'
helloハロー
u'hello\u30cf\u30ed\u30fc'


おっ? 一部文字化けしてしている。
大丈夫だ、想定内だ。


これはコマンドプロンプトエンコード(cp932)とファイルのエンコード(utf-8)が違うため。
(ちなみに、ロケールUTF-8Linuxでは文字化けせずに正しく表示されます。)

(余談)
出力する端末のエンコードを調べるには、
import sys; sys.stdout.encoding とします


文字化けしないように正しくエンコード変換をしてやる。
(ファイルの中身)

# coding=utf-8
import sys
str = "helloハロー"
uni = u"helloハロー"

uniStr = unicode(str, "utf-8", 'ignore')    ## utf-8 -> unicode(UCS-2)へ変換
newStr = uniStr.encode("cp932")             ## unicode(UCS-2) -> cp932へ変換

print newStr                                ## cp932で出力
print repr(newStr)                          ## cp932のバイト列かをチェック

print uni
print repr(uni)


(実行結果)

C:\Python24>python enc.py
helloハロー
'hello\x83n\x83\x8d\x81['
helloハロー
u'hello\u30cf\u30ed\u30fc'


文字化けせず出力された。
格納されているバイト列も、先ほどのShift-JISで保存したファイルの文字コードと一致して、
想定どおりの結果でオッケー。


3.デフォルトエンコーディング

さて、最後に3.デフォルトエンコーディング


デフォルトエンコーディングとは、暗黙のエンコーディングのこと。
Pythonでは暗黙のエンコーディングがちょくちょく起こる。
例えば、先ほどから何回もでているprintでも暗黙のエンコーディングが行われている。


そもそも、暗黙のエンコーディングとはなんぞや?
これは、Pythonのシステムエンコーディングや環境や端末のエンコーディングを取得して、
自動的に適切なエンコーディングにしてくれるというもの。


例えば、codecsモジュールのcodecsオブジェクトは
open( filename, mode[, encoding[, errors[, buffering]]]) というメソッドをもってるが、
ここにある第3引数"encoding"は何もしていないと、
デフォルトエンコーディングが利用される。


Pythonでは、何も設定しないとデフォルトエンコーディングは'ascii'になっている。

デフォルトエンコーディングを取得するには
import sys; sys.getdefaultencoding() とする


デフォルトエンコーディングは、主に次の4つ場合に起こる(もちろんこれ以外にもある)。

  1. 標準入出力
  2. ファイルの入出力
  3. codecsモジュールでの入出力
  4. string文字列とユニコード文字列間の変換
(余談)
pythonで利用されている各種エンコードの取得するには
import sys
print "encoding: " + sys.getdefaultencoding()        # デフォルトのエンコード
print "file system: " + sys.getfilesystemencoding()  # ファイルシステムのエンコード
print "stdin: " + sys.stdin.encoding                 # 入力端末のエンコード
print "stdout: " + sys.stdout.encoding               # 出力端末のエンコード


というわけで、デフォルトエンコーディングについて調べてみる。

実験コードは2.で使用したenc.pyを改変して利用する
(ファイルの中身)

# coding=utf-8
import sys
print "encoding: " + sys.getdefaultencoding()
print "file system: " + sys.getfilesystemencoding()
print "stdin: " + sys.stdin.encoding
print "stdout: " + sys.stdout.encoding
print ""

str = "helloハロー"
uni = u"helloハロー"

uniStr = unicode(str)     # strのバイトコードを -> Unicode(UCS2)に変換

print uniStr
print repr(uniStr)

print uni
print repr(uni)

(実行結果)

C:\Python24>python enc.py
encoding: ascii
file system: mbcs
stdin: cp932
stdout: cp932

Traceback (most recent call last):
  File "enc.py", line 17, in ?
    uniStr = unicode(str)
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 5: ordinal not in range(128)


当然のエラー。
大丈夫。想定内だ。


注目したいのはここ12行目、「uniStr = unicode(str)」。
前回は、「uniStr = unicode(str, "utf-8", 'ignore')」としていた。
違いはなにか?


この2つは

  1. unicode(str)は、strをデフォルトエンコーディングエンコードして、unicode(UCS2)形式で保存する。
  2. unicode(str, "utf-8", 'ignore')は、strを"utf-8"でエンコードして、unicode(UCS2)形式で保存する。


ということ。


結局1.は、、「ハ」は2バイトなのに、デフォルトエンコーディングがasciiなため、
1バイトのみを取ってきて、asciiエンコードしようとし、失敗したという結果。


じゃあ、毎回unicodeの時、"utf-8"を指定すればいいけど、めんどくさい。


そこで、このデフォルトエンコーディングをサイト毎に設定するのが
pythonの解説サイトの各所で書かれているsitecustomize.py。

(余談)
例えばデフォルトエンコーディングUTF-8にするには、
pythonディレクトリのlib\site-packagesにsitecustomize.pyというファイルを作成し、
import sys
sys.setdefaultencoding('utf-8')
と記述しておく。

というわけで、デフォルトエンコーエンコーディングutf-8にしてもう一度実行。
(実行結果)

C:\Python24>python enc.py
encoding: utf-8
file system: mbcs
stdin: cp932
stdout: cp932

helloハロー
u'hello\u30cf\u30ed\u30fc'
helloハロー
u'hello\u30cf\u30ed\u30fc'


大丈夫。想定内だ。


ばっちり、UTF-8形式で保存・表示された。

ちなみに端末がcp932なのに、日本語表示されてるのは、printが適切にエンコーディングしてくれてるため。


おーけー、これで完璧!


でもちょっいまち、
ほんとに、各所ブログで書いてあるように、
安易にsitecustomize.pyを設定していいのか?


たぶん、答えは「ノー」。
だって、少なくともPython2.5まではdefaultencodingは「ascii」なのである。(Python3000では違うっぽい。これは別途)


結局、Pythonでの移植性を殺すことになる。
もちろんアップロードできるサーバがすべて、自分の管理下にある場合ならばいいけれど、
オープンソースなものやホスティングで、defaultencodingが
自分の想定しているエンコードが設定されているという保証はどこにもない。
というわけで、愚直にエンコードを指定することをおすすめする。

(余談)
あんまりおすすめしないが次のやり方すれば、Lib\以下にsitecustomize.pyを設置しなくても、
カレントフォルダにsitecustomize.pyを置けばいいので、簡単にデフォルトエンコーディングを変更出来る。

import sys
import os
sys.path = [os.getcwd()] + sys.path
import sitecustomize
reload(sitecustomize)

まとめ

というわけでまとめ。


というわけで、自戒をこめたメモですが、
お役に立ちましたでしょうか?
これであなたもLet's Pythoniグー!!! グー、グー、グーグググググググーーー


コーーーーーーーーッ!!!


って無理があったなw


(余談)
Pythonunicodeメソッドのencode、decodeは本当にややこしい。
なんで、utf8で書かれてるファイルの文字列をstr.decode('utf-8')して格納し、
出力するときはuniStr.encode('utf-8')なのか?

エンコードは符号化でデコードは復号だから、逆じゃねーの??って思う。


そこでこう考える。
str.decode('utf-8')は Pythonの内部コードUCS2へ、strのバイトコードutf-8にデコードして格納、
str.encode('utf-8')は Pythonの内部コードUCS2から、utf-8エンコードして出力。
つまり、処理の主体が内部(python)と考えれば納得。

追記

実はこっちのほうがよくまとまってたり
日本語文字列コード問題まとめ