目次へ戻る §14へ戻る §15.1 §15.2 §15.3 §15.4 §15.5 §15.6 §15.7 §16へ進む

15.2 Cygwin の Python と日本語

Python 2.4.1 は Unicode および (各国語対応のひとつとして) 日本語エンコーディングをサポートしています。 ただし,詰めの甘い点があり,そのままでは Cygwin で十分使えません。 以下,問題点を説明し, その簡単かつ効果的 (1個のファイルへの数行の設定だけで, そのマシンの全ユーザに恒久的に有効,あとからの設定の取消しも容易) な解決策を示します。

Cygwin の問題点

前節 の hello.py スクリプトでは, 普通の文字列,つまりバイト値の並びで日本語文を書きました。

#!/usr/bin/env python
# -*- coding: shift_jis -*-
print "こんにちは,世界"

Python は Unicode をサポートしています。 そこで Unicode 文字列で書き直してみます。 文字列リテラルの先頭に u をつけるだけです。

#!/usr/bin/env python
# -*- coding: shift_jis -*-
print u"こんにちは,世界"

これを s_hello.py というファイル名で作成して実行してみます。

01:~$ ./s_hello.py
Traceback (most recent call last):
  File "./s_hello.py", line 3, in ?
    print u"こんにちは,世界"
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-7:
ordinal not in range(128)
11:~$ 

残念ながらエラーが起こります。なぜでしょうか?

Windows のコマンドプロンプトは Unicode 文字を表示できる能力を もっていますが,Cygwin の API を経由する限り,バイト列に変換しなければなりません。 上記のエラーは,Unicode 文字列をバイト列に符号化 (encode) するとき, range(128) つまり 0 から 127 までの文字コードしか扱えない 'ascii' エンコーディングが使われ,日本語文字に対して例外 UnicodeEncodeError が起こったものです。

こうなってしまう直接の原因は,Python の print 文で使われる 標準出力 sys.stdout のエンコーディングが, Cygwin では常に 'US-ASCII' になるからです。

>>> import sys
>>> sys.stdout.encoding
'US-ASCII'
>>>

§5.3 で述べたように, 今のところ Cygwin は環境変数による locale 設定ができません。 Unix の正攻法で,API 経由で locale 関連情報を取得した場合, 事実上,英語 (といいますか米語) 決め打ちになります。

一方,Python のソースファイル Python-2.4.1/Python/pythonrun.c の Py_InitializeEx 関数をみると, sys.stdin.encoding, sys.stdout.encoding, sys.getfilesystemencoding() の値を,nl_langinfo() で取得した情報で設定しています。 ですから,事実上,それらのエンコーディングは 'US-ASCII' 決め打ちになります。 環境変数 LANG や LC_ALL 等はあってもすべて無視されます。

解決に向けて -- カスタマイズ用ファイル sitecustomize.py

基本的に,Python に対するカスタマイズは下記のファイルを設けて行います。

/usr/lib/python2.4/site-packages/sitecustomize.py

/usr/lib/python2.4/site-packages/ は非標準パッケージの 標準的なインストール場所です。初期状態では README ファイルだけが あります。 libxml2-python など,Cygwin のパッケージで Python を拡張するものは, 原則としてこの場所に拡張モジュールをインストールします。

ちょっと考えると sitecustomize.py で sys.stdin.encoding, sys.stdout.encoding, sys.getfilesystemencoding() の値を書き換えればよさそうです。 これらの属性値,関数値は Python モジュールからは書換え不能に設定されていますが, sitecustomize.py から (dll ファイルとして用意した) C モジュールを import し, そこで設定してしまえばよさそうです (実際,属性書換え自体は実験してうまくいくことを確かめました)。

しかし,ここで再び Py_InitializeEx 関数をよく読むと, sitecustomize.py の呼出しをひき起こす initsite() の実行後に, sys.stdout.encoding 等の設定をしています。 プログラムごとにいちいち属性書換え C モジュールを呼び出すならばともかく, sitecustomize.py による事前の一括設定がどうしてもできない設計になっています :-(

ここで少し視点を変えて, 日本語 Windows の「メモ帳」でファイルをセーブするときの ことを思い出してください。utf-8 等ではなく Shift JIS でセーブするとき, 文字コード欄に ANSI を選択します。 米国の規格のことを Shift JIS と読み替えているわけです。 もしも,どうしてもエンコーディングが US-ASCII 決め打ちになるならば, 同じようなことを Python でやってしまえばよさそうです。

解決策 -- Cygwin 専用ハック

下記の内容のファイルを /usr/lib/python2.4/site-packages/sitecustomize.py として置いてください。既存の sitecutomize.py があるときは, その内容に追加してください。問題はこれで解決します。

from encodings import aliases
aliases.aliases['us_ascii'] = 'cp932'

Python 内部でエンコーディング名を検索するとき, 大文字を小文字にし,ハイフンをアンダースコアにして, encodings.aliases モジュール (ファイルは /usr/lib/python2.4/encodings/aliases.py) の 辞書 aliases から検索します。 'us_ascii' はここで 'ascii' に写像されます。 それを書き換えて,'cp932' に写像するようにするわけです。

ASCII だけで書かれたバイト列は,そのまま Shift JIS のバイト列としても 通用しますから,'us_ascii' を 'cp932' に読み替えても問題はありません。 もし問題があったとしても, sitecustomize.py の内容をコメントアウトするだけで簡単に元に戻せますから, 原因解明は容易です。 *4

なお,御存知のように cp932 は Shift JIS の Windows 固有のバリアントですが, ここでの設定は sys.stdin.encoding, sys.stdout.encoding, sys.getfilesystemencoding() つまり端末入出力とファイル名のエンコーディングに関係しますから, 'shift_jis' ではなく 'cp932' を採用します。 より詳しい議論については §15.3 を御覧ください。

お急ぎの方は,ここにある sitecustomize.py をコピーしてお使いください。 これには 後述の追加設定もあわせて入っています。

利用例

端末の出力だけでなく入力の encoding 属性も読み替えられていますから, 今まで

>>> "ソソソ"
  File "", line 1
    "ソソソ"
           ^
SyntaxError: EOL while scanning single-quoted string
>>>

となっていたのが,これからはこうなります。*5

>>> "ソソソ"
'\x83\\\x83\\\x83\\'
>>>

さきほどの s_hello.py も今度は無事実行できます。

01:~$ ./s_hello.py
こんにちは,世界
01:~$ 

この例だけでは,単なるバイト列である普通の文字列と Unicode 文字列との違いが明確ではありません。 ここで s_hello.py のエンコーディングとその宣言を utf-8 や euc-jp に変更してセーブしてみましょう。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
print u"こんにちは,世界"

この場合,ファイルの内容をじかにみると,当然,文字化けします。

01:~$ cat ./u_hello.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
print u"縺薙s縺ォ縺。縺ッ・御ク也阜"
01:~$ 

しかし,実行してみると正しく動きます。

01:~$ ./u_hello.py
こんにちは,世界
01:~$ 

これから分かるように,Unicode 文字列を使えば, 非 Shift JIS の環境とスクリプトファイルやモジュールファイルを 相互に利用できます。 (ただし,後述 するような問題点に注意してください)

対話セッションでの Unicode 文字列の利用例を示します。前節の 普通の文字列のとも比較してください。

>>> u = u"はにほへといろは"
>>> u
u'\u306f\u306b\u307b\u3078\u3068\u3044\u308d\u306f'
>>> print u
はにほへといろは
>>> u[:3]
u'\u306f\u306b\u307b'
>>> print u[:3]
はにほ
>>> print u[-3:]
いろは
>>>

暗黙の Unicode 変換について -- sitecustomize.py への追加設定

Unicode 文字列 u から普通の文字列への変換は次の方法でできます。

1. Unicode 文字列メソッド encode による変換
u.encode([encoding [, errors]])
2. 型オブジェクト str による変換
str(u)

普通の文字列 s から Unicode 文字列への変換は次の方法でできます。

1. 文字列メソッド decode による変換
s.decode([encoding [, errors]])
2. 型オブジェクト unicode による変換
unicode(s [, encoding [, errors]])
3. Unicode 文字列との加算演算
s + u''

省略可能引数 errors は変換失敗時の振舞を指定します。 詳細は help 等をみてください。

どのプラットフォームの初期設定でも encoding の省略時値は 'ascii' です。 いわば最大公約数である 'ascii' を使うとき以外は, それを陽に指定させようという設計意図です。 エンコーディング宣言が導入される以前に一般的だった非 Unicode の 文字列リテラルに対しては確かにこれでよかったかもしれません (脚注 *4 参照)。

困るのは,実行時のキー入力などからその場で作られた文字列を Unicode に変換するときです。 Shift JIS 環境では Shift JIS 文字列が, EUC-JP 環境では EUC-JP 文字列が, utf-8 環境では utf-8 文字列が作られます。 もしもプログラム中でどれか一つに encoding を決め打ちした場合, 他の環境では動かせなくなります。 同一プログラムが異なる環境で 異なるエンコーディング処理をしなくてはならない わけですから,前述の例のようなエンコーディング宣言 とは事情が異なります。

encoding 引数に変数を指定し, それを環境ごとに変更すればよいわけですが, プログラムが大きくなると一貫性の維持に労力がかかるようになります。

さらに事態を悪くするのは,Unicode 文字列との加算 s + u'' による暗黙の変換です。 ここでは encoding を指定できません。しかし, 手軽に書けるため,Python の標準ディストリビューションを含む実際のコードで よく使われています。ASCII 環境で動かす限りは問題なく動きます。 まぎれもない現実として非 ASCII 環境で顕在化するバグの温床になっています。 悪いことに構文に特徴がないため,これが実行される場所の静的な列挙は困難です。

しかし,さきの場合と異なり, これらの encoding の既定値を変更するクリーンな解決策が標準で 用意されています。 下記を /usr/lib/python2.4/site-packages/sitecustomize.py の内容に追加してください。

import sys
sys.setdefaultencoding('shift_jis')

ここでなぜ 'cp932' ではなく 'shift_jis' かについては §15.3 を御覧ください。

注意:
これは言語であらかじめ用意されているオプションのひとつであり, どのプラットフォームでも既定の設定は, 今のところ 'ascii' であることに重ね重ね注意してください。 自作の Python スクリプトやモジュールを 広く 一般に配布しようとする場合は, 既定のままであっても動作すること,あるいは少なくとも適切なレポートを 伴って終了または例外送出することを確認する必要があります (実際には標準配布コードでさえ完全には達成できていない理想論ですが…)。 プログラム中では sys.getdefaultencoding() の戻り値で設定を確認できます。

追加設定の使用例

下記は追加設定前の例です。 Unicode の空文字列と,非 ASCII の普通の文字列をつなげて, Unicode 文字列を作ろうとしていますが,'ascii' だけでは解釈できないため, 例外 UnicodeDecodeError が発生しています。 sys.getdefaultencoding() の戻り値はたしかに 'ascii' になっています。

>>> u"" + "ぱ"
Traceback (most recent call last):
  File "", line 1, in ?
UnicodeDecodeError: 'ascii' codec can't decode byte 0x82 in position 0:
ordinal not in range(128)
>>>
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>>

下記は追加設定後の例です。 Unicode の空文字列と非 ASCII の普通の文字列をつなげて, Unicode 文字列が作られています。 sys.getdefaultencoding() の戻り値はたしかに 'shift_jis' になっています。

>>> u"" + "ぱ"
u'\u3071'
>>>
>>> import sys
>>> sys.getdefaultencoding()
'shift_jis'
>>>

15.3 日本語用エンコーディング

Python 2.4.1 に標準で用意されている日本語のエンコーディング (とその主な別名) は次のとおりです。 JIS X 0213 の 2004 年発行の追補1 (JIS X 0213:2004) *6 にまで対応した本格的な労作です。

標準エンコーディングの問題点

よく知られているように,JIS 漢字と Unicode 文字の対応は1対1ではありません。 上記のエンコーディングのうち,cp932 とそれ以外とでは, 同じ JIS 漢字に対して一部ことなった写像を採用しています。 このことは, 例えばこれまで国内の多数の Java プログラマを悩ませてもきました。 以下この問題を概説し,対応策を示します。

shift_jis や shift_jis-2004 と,cp932 は, 基本的にはどれも シフト JIS ですが, いくつかのマルチバイト文字を (同じような字形の) 別々の Unicode 文字に写像します。

>>> "〜£−¬"
'\x81`\x81\x92\x81|\x81\xca'
>>> "〜£−¬".decode('shift_jis')
u'\u301c\xa3\u2212\xac'
>>> "〜£−¬".decode('shift_jis-2004')
u'\u301c\xa3\u2212\xac'
>>> "〜£−¬".decode('cp932')
u'\uff5e\uffe1\uff0d\uffe2'
>>>

shift_jis と shift_jis-2004 は共通ですが, cp932 は互換性がありません。 この互換性,非互換性は,euc-jp など他のエンコーディングにも及んでいます。

文字集合の範囲内にある限り,基本的に, shift_jis や shift_jis-2004 は Unicode 文字列を仲立ちにして, euc-jp など他のエンコーディングに自由に変換できます。 下記の最初の二つの式では,shift_jis からの Unicode を euc-jp に変換し, さらにそれをまた Unicode に戻しています。 もとの Unicode に戻っていますから,相互変換可能なことが分かります。 しかし,cp932 からの Unicode 文字列は,euc-jp への変換に失敗します。 同じ理由で shift_jis への変換にも失敗します。

>>> "〜£−¬".decode('shift_jis').encode('euc-jp')
'\xa1\xc1\xa1\xf2\xa1\xdd\xa2\xcc'
>>> "〜£−¬".decode('shift_jis').encode('euc-jp').decode('euc-jp')
u'\u301c\xa3\u2212\xac'
>>> "〜£−¬".decode('cp932').encode('euc-jp', 'ignore')
''
>>> "〜£−¬".decode('cp932').encode('shift_jis', 'ignore')
''
>>>

ここではエラーメッセージの煩雑さを避けるため, errors 引数に 'ignore' を指定しました。 1文字も変換に成功していませんから結果が空文字列になっています。

ただし,Python の cp932 変換には, 「君のものは僕のもの,僕のものは僕のもの」という言葉を思い起こさせる, ちょっとした工夫があります。 下記をよく見てください。 さきほどの例では cp932 由来の Unicode 文字列の shift_jis への変換に失敗 しましたが,下記のように shift_jis 由来の Unicode 文字列を cp932 に変換することには成功します。 「shift_jis のものは cp932 のもの,cp932 のものは cp932 のもの」 というわけです。

>>> "〜£−¬"
'\x81`\x81\x92\x81|\x81\xca'
>>> "〜£−¬".decode('shift_jis')
u'\u301c\xa3\u2212\xac'
>>> "〜£−¬".decode('shift_jis').encode('cp932')
'\x81`\x81\x92\x81|\x81\xca'
>>> 

種明かしすると, Unicode からマルチバイト文字へエンコードするとき,'cp932' は cp932 本来の写像と,shift_jis 方式の写像をあわせた多対一の写像を 使います。 こういった工夫は現在の日本で Unicode を実用的に扱うための 常套,というよりむしろ必須の手段といってよいでしょう。 似たような例で,確認できた最も早期のものは 2003 年 1月に fj.sources に流された ja-codecs でした。2004 年 1月の 2.0.4 版以降の nkf にも同じ工夫を見ることができます。

文字集合の大きさでいえば,shift_jis が共通部分集合であり, shift_jis-2004 と cp932 はその拡張になっています。 しかし両者には両立できない違いがあります。

>>> "①Ⅱⅲ"
'\x87@\x87U\xfaB'
>>> "①Ⅱⅲ".decode('shift_jis', 'ignore')
u''
>>> "①Ⅱⅲ".decode('shift_jis-2004')
u'\u2460\u2161\u8c6d'
>>> "①Ⅱⅲ".decode('cp932')
u'\u2460\u2161\u2172'
>>>

文字集合の範囲外ですから,shift_jis は "①Ⅱⅲ" を変換できません。

shift_jis と cp932 は "①Ⅱ" については共通の変換をしています。しかし, 小文字のローマ数字 "ⅲ" を変換できているのは cp932 だけです。 shift_jis-2004 は "ⅲ" の文字コード \xFA \x42 を 「豭」という Unicode 漢字 (文字コード \u8C6D) に写像しています。

もちろん, これは cp932 が正しくて shift_jis-2004 が間違っているという意味ではありません。 両者がシフト JIS の 文字コード \xFA \x42 に全く別の文字を割り当てている,というだけのことです。 Cygwin を走らせている Windows のコマンドプロンプトがたまたま cp932 を 採用しているため,shift_jis-2004 が意図している文字 "豭" とは別の "ⅲ" が表示されているわけです。

標準エンコーディングに対する対応策

要点をまとめます。

  1. Cygwin の API を経由する限り, 端末入出力やファイル名は cp932 エンコーディングになります。
  2. 'shift_jis' や 'euc-jp' などから作られた Unicode を 'cp932' は 文字集合の範囲内でエンコードできます。
  3. 'cp932' から作られた Unicode は,たとえ文字集合の範囲内でも 他ではエンコードできないことがあります。

2. と 3. をあわせて「君のものは僕 (=cp932) のもの,僕のものは僕のもの」 と暗記できます。 つまり Python では shift_jis のことを cp932 の部分集合であって, 他との移植性,互換性があるもの,とみることができます。

/usr/lib/python2.4/site-packages/sitecustomize.py での US ASCII の読み替え の設定では,'cp932' が妥当です。 丸数字やローマ数字を含んだ Unicode 文字列も問題なく表示されます。 対話セッションでそのような Unicode 文字列をダイレクトに入力できます。 'shift_jis' 由来の Unicode 文字列も表示できます。ただし, もしも対話セッションでどうしても 'shift_jis' 由来の Unicode 文字列を 作りたいときは,unicode("文字列", 'shift_jis') のように陽に変換します。

プログラムでキー入力やファイル入力をする場合, raw_input() をはじめとして,基本的に普通の文字列として入力されます。 それを Unicode 文字列に変換する場合,デフォルトエンコーディングとして sys.setdefaultencoding で指定したものが使われます。 さまざまな環境から入力されるデータを処理し,出力する場合, 同じ文字が同じものとして扱われる必要がありますから, この場面では,他と互換性がある 'shift_jis' が妥当です。 もしもプログラム中でどうしても cp932 変換が必要な場合は, unicode("文字列", 'cp932') のように陽に変換します。

同様にプログラム中の文字列定数についても,もしシフト JIS を使うとすれば, 'shift_jis' を使うのが移植性の点で安全です。 エンコーディング宣言に -*- coding: shift_jis -*- と書きます*7。 'cp932' 由来の Unicode 文字列は,他の日本語環境にもっていったとき, 表示できないかもしれませんし,同一文字のはずが別の Unicode になって照合できないかもしれません。

もしもプログラム中で丸数字 ① や大文字ローマ数字 Ⅱ 等を使いたい場合は, -*- coding: shift_jis-2004 -*- (あるいはその 2000 年版である -*- coding: shift_jisx0213 -*-) とするのが,他との一貫性の点で 好ましいでしょう。実際,この範囲までならば,Mac とも互換性をとれます。 ただし,前述 の 小文字ローマ数字 ⅲ の例にみるように, 必ずしも部分集合でないことに注意してください。

cp932 由来または出所不明の Unicode 文字列 u があるとき, それを euc-jp 文字列 e に安全に変換するには,

w = u.encode('cp932', 'replace').decode('shift_jis', 'replace')
e = w.encode('euc-jp', 'replace')

のように,いったん 'cp932' としてシフト JIS 文字列に変換してから, あらためて 'shift_jis' 経由で Unicode 文字列に逆変換します。 こうすると JIS 第1第2水準漢字と半角カナの範囲で文字化けを防ぐことができます。 この例では 'replace' を指定していますから, 文字集合の範囲外で変換できなかった文字は「?」になります。


脚注

*4 エンコーディング宣言をサポートする以前の旧版の Python では, プログラム中の非 ASCII 文字に対する取り扱いは単なるバイト透過でした。 コメントや文字列では, バイト透過性を利用して Latin-1 のほか EUC-JP や utf-8 が使われていました。 当時導入されはじめた Unicode 文字列リテラルは (文字列の各バイト値を そのまま Unicode の下位バイトとする実装により) 事実上 Latin-1 扱いでしたから, EUC-JP 等を使用している場合,下記のように陽に変換することで代用していました。

u = unicode("EUC-JPで書かれた文字列", 'euc-jp')

そのような状況下でバイト文字列を Shift JIS で解釈するように改造したとすると, さまざまな問題がひき起こされたことでしょう。 (実際,そのような改造例もありました)
まず,実際に Latin-1 で書かれたファイルが, 不正なマルチバイト文字を含んでいるとしてエラー扱いされることになります。 マルチバイト文字として正常に解釈できたとしても, あるいは互換性向上のためにそのようなエラーを無視するようにしたとしても, たとえば Latin-1 で字上符付きの 'a' にあたる '\xe0' から '\xe5' のコードに 文字列エスケープの '\' が隣接しているとき, '\' が Shift JIS のマルチバイト文字の第2バイトと誤解されて エスケープが消えてしまいます。この問題はすぐに顕在するかもしれませんし, 長期間潜在するかもしれません。 EUC-JP 等のファイルに対しても同じような問題があります。 EUC-JP の2バイト文字の第1バイトを半角カナと誤解した場合などを 考えてみてください。
たとえ,利用者が書いたファイルについて透過的な相互運用をあきらめたとしても, ライブラリがツリー構造のデータを Latin-1 文字列を含むタプルやリストの リテラルとしてファイル出力し,あとからファイル内容を eval することで データを復元するような場合,やはり問題をひき起こします。 これに対処するためには内部実装に踏み込んだ個別対応が必要になってきます。
一方,もともと '\x00' から '\x7F' に制限されている US-ASCII を Shift JIS として解釈するように 改造したときは,このような問題は発生しません。

*5 注意深い方なら,これまでも端末入力時のエンコーディングが 'ascii' でなかったことにお気づきになっているかと思います。 対話セッションで非 ASCII 文字を入力できていたからです。 実は,入力は,'ascii' と設定されていても, 旧版 Python と同じく単なるバイト透過 (といいますか Latin-1) になります。 "ソソソ" の例から分かるように バイト透過と Shift JIS は非互換ですから,一見ここで問題が発生しそうに思えます。
しかし, 標準入力 sys.stdin の encoding が設定されるのは, sys.stdin.isatty() が True のとき,つまり端末入力のときだけであり, リダイレクトされた入力に対しては従来どおりです。 実際に日本語 Windows 上の Cygwin で非互換性によるバグが発生するとは 考えにくいゆえんです。

*6 JIS X 0213 にもとづくエンコーディングの概要については, たとえば矢野氏の 「JIS X 0213の代表的な符号化方式」 を参照してください。

*7 もともとエンコーディング宣言は, Emacs エディタと Python 処理系で2重に宣言するのはナンセンスだからと, Emacs の宣言書式を借りたものでした。その趣旨からも 'shift_jis' が妥当といってよいかもしれません。 おそらく日本語対応のどの Emacs 実装でも理解されるからです。 ただし,字句解析処理に変換・逆変換が入らないという透過性と効率の点では, utf-8 が有利です。

目次へ戻る §14へ戻る §15.1 §15.2 §15.3 §15.4 §15.5 §15.6 §15.7 §16へ進む

Copyright (c) 2005 Oki Software Co., Ltd.