コーデックmbcsでは例外が発生しない

_winreg の件を調べていて気付いたんだけど、コーデック mbcs は変換できない文字に出会っても例外を発生させないね。

# Python 2.7.1
# ↓ハングルと簡体字が含まれてます
data = u'\ud55c\uae00 and \u7b80\u4f53\u5b57'
print data.encode('mbcs')  # 出力: ?? and ?体字

上のコードを実行しても例外は発生しない。変換できない文字は自動で ? に置き換えられている。

何をやっているのか調べてみたら、 WideCharToMultiByte() を呼んでいる場所に行き着いた

static int encode_mbcs(PyObject **repr,
                       const Py_UNICODE *p, /* unicode */
                       int size) /* size of unicode */
{
    /* 省略 */
    if (0 == WideCharToMultiByte(CP_ACP, 0, p, size, s, mbcssize, NULL, NULL))
    /* 省略 */
}

変換できない文字があったことを知るためのフラグ lpUsedDefaultChar にNULLが指定されている。どうりで例外が発生しないわけだ。

Python 3.2 ではこの引数をチェックして例外を出すように変更されているので、以下の通りちゃんと例外が発生する。

# Python 3.2
data = '\ud55c\uae00 and \u7b80\u4f53\u5b57'
print(data.encode('mbcs'))
# UnicodeEncodeError: 'mbcs' codec can't encode characters in position 0--1: invalid character

Python でコードを書いていると時々、多言語の処理がいい加減な部分にぶつかって嫌になるけど、少しづつ良くなってはいるな。

Python2.7の_winregでは読めない文字がある

_winreg にある関数に REG_SZ 値を取得させると、 unicode を返すようになっている。でも、内部では ANSI 版の Widnows API を使っているので、cp932(≒Shift_JIS)に含まれない文字が化ける。例えば、韓国語のハングルや、中国語の簡体字を日本語版の Windows で読ませるとこの通り。

import _winreg
with _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, r'Software\_pytest') as key:
    print _winreg.QueryValueEx(key, 'hangul')[0]  # ??
    print _winreg.QueryValueEx(key, 'hanzi')[0]   # ?体字

一部が ? になってしまっている。

書くほうはどうかというと、

import _winreg
with _winreg.OpenKeyEx(_winreg.HKEY_CURRENT_USER,
                       r'Software\_pytest',
                       0, _winreg.KEY_WRITE) as key:
    # はてなダイアリーは未だにEUC-JPなので適当にエスケープ
    data = u'\ud55c\uae00 and \u7b80\u4f53\u5b57'
    _winreg.SetValueEx(key, "mix", 0, _winreg.REG_SZ, data)

こちらも一部が ? になってしまう。

ANSI 版の Windows API は大抵 UNICODE 版のラッパーなので _winreg を使うと2回も文字コードを変換していることになって効率が悪いし、扱える文字の幅を意味もなく制限していることになると思う。

回避策

まず PyWin32 を試した。

import win32api
import win32con
handle = win32api.RegOpenKeyEx(
    win32con.HKEY_CURRENT_USER,
    r"Software\_pytest",
    0,
    win32con.KEY_READ)
print type(win32api.RegQueryValueEx(handle, u'hangul')[0])  # <type 'str'>
handle.close()

戻り値が str なので駄目だ。

WMI (Windows Management Instrumentation) 経由でレジストリが読めるというのを聞いたことがある。ちょうどここにライブラリがある(動作には PyWin32 が必要)。やってみるときちんと読める。

import wmi
HKEY_CURRENT_USER = 0x80000001
conn = wmi.WMI()
err, value = conn.StdRegProv.GetStringValue(
    HKEY_CURRENT_USER, r"Software\_pytest", "hanzi")
if not err:
    print value.encode('cp932', 'xmlcharrefreplace')  # &#31616;体字

ここまで来て気付いたのだが、コードページが 932 なコマンドプロンプトにハングルと簡体字は表示できないな。上のコードでは適当に置き換えておいた。

次、ctypes 。 RegQueryValueExW() でワイド文字列を読むのは面倒くさいので、Vista から使えるようになった RegGetValueW() を使うことにする。

import ctypes
from ctypes import wintypes

HKEY_CURRENT_USER = 0x80000001
RRF_RT_REG_SZ = 0x00000002
ERROR_SUCCESS = 0L

buf = ctypes.create_unicode_buffer(32)
size = wintypes.DWORD(ctypes.sizeof(buf))  # この sizeof はバイト数を返す

RegGetValue = ctypes.windll.Advapi32.RegGetValueW
RegGetValue.restype = wintypes.LONG
ret = RegGetValue(HKEY_CURRENT_USER, ur'Software\_pytest',
                  u'hanzi', RRF_RT_REG_SZ, None, buf, ctypes.byref(size))
if ret == ERROR_SUCCESS:
    print buf.value.encode('cp932', 'xmlcharrefreplace')  # &#31616;体字

問題なし。

Python 3.x の winreg では、ここに書いたような問題は起きないはず。UNICODE 版の Windows API を使っているからね。早く 3.x に移行したいなあ。

.pycファイルを作成する方法メモ

時々しかやらないから方法をいつも忘れるな。

モジュール compileallpy_compile を使うと .pyファイルをコンパイルして .pycファイルを作成できる。これらのモジュールは直接実行できるようになっているので、例えば foo.py をコンパイルするにはこうする。

python -m compileall foo.py
もしくは
python -m py_compile foo.py

カレントディレクト以下にあるファイルを全部コンパイルするならこう。

python -m compileall .

レジストリの用語が統一されていない

レジストリについて調べていたら、文書毎に用語が統一されていないことに気付いた。

とりあえず見つけたものを表にしてみた。

自分が使っている用語 別名
ルートキー メインキー、定義済みキー
値エントリ、レジストリエントリ
既定値 標準の値、既定の値、名前なしのレジストリエントリ、標準のレジストリエントリ、既定のエントリ、名前なしの値、デフォルトの値
値の名前 レジストリエントリ名、エントリの名前
値の種類 値型、値の型、値のタイプ、レジストリエントリのデータ型
値のデータ 値、設定値、エントリの値、エントリ値、レジストリエントリのデータ

値のことをレジストリエントリと呼ぶのは一般的みたい。MSDN ライブラリやTechnetでもよく見かける。

値のことをレジストリエントリと呼んでいる記事では、たまに値のデータのことを単にと呼んでいる場合があるので注意。さらに、値の名前のことをキー名と表記している記事もあった。

少し前に長音符のルールが変更されたので今後、エントリーレジストリエントリーという表記が出てくるかも。

明後日の場所でUnicodeDecodeError

Google App Engine の webapp フレームワークを使ってコードを書く。

↓これは大丈夫だが、

class Page1(webapp.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.out.write('abc')
        self.response.out.write(u'あいう')

↓これはだめ。

class Page2(webapp.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.out.write(u'abc')
        self.response.out.write('あいう')

# GAE/Python 1.4.1

ファイルオブジェクトとはまったく逆の挙動だな。

traceback はこんな感じ。

<type 'exceptions.UnicodeDecodeError'>: 'ascii' codec can't decode byte 0x82 in position 0: ordinal not in range(128)

Traceback (most recent call last):
  File "/base/data/home/apps/<app name>/<version number>/<file name>", line 27, in main
    util.run_wsgi_app(application)
  File "/base/python_runtime/python_lib/versions/1/google/appengine/ext/webapp/util.py", line 97, in run_wsgi_app
    run_bare_wsgi_app(add_wsgi_middleware(application))
  File "/base/python_runtime/python_lib/versions/1/google/appengine/ext/webapp/util.py", line 115, in run_bare_wsgi_app
    result = application(env, _start_response)
  File "/base/python_runtime/python_lib/versions/1/google/appengine/ext/webapp/__init__.py", line 535, in __call__
    response.wsgi_write(start_response)
  File "/base/python_runtime/python_lib/versions/1/google/appengine/ext/webapp/__init__.py", line 248, in wsgi_write
    body = self.out.getvalue()
  File "/base/python_runtime/python_dist/lib/python2.5/StringIO.py", line 270, in getvalue
    self.buf += ''.join(self.buflist)

例外は自分が書いたコード以外の場所で発生しているようだ。

調べてみると StringIO が原因だった。 self.response.out には StringIOインスタンスが入っている。ソースを読むと StringIO は基本的に誰でもウェルカムで、 str でも unicode でも辞書でも数字でも何でも書き込めることがわかる。

from StringIO import StringIO
s = StringIO()
s.write('abc')
s.write(u'def')
s.write({'a' : 1})
s.write(123)
print repr(s.getvalue())  # u"abcdef{'a': 1}123"

basestring でないものが入力された場合は黙って str() にかけてからバッファに積む。 getvalue()read() されたらバッファを連結して返す、というシンプルな動作。

これで問題になるのは、ASCII 以外の文字を含む str と、unicode を書き込んだ時である。 StringIO はこの2つが入力されても特に気にすることなく連結しているので、当然 UnicodeDecodeError が発生する*1。ややこしいことに、実際の連結作業が行われるのは read()getvalue() した時で、 write() した時にはエラーにならない。

from StringIO import StringIO
s = StringIO()
s.write(u'abc')
s.write('あいう')
#
# 何か別の処理 ...
#
body = s.getvalue()  # ここでエラー

これは単純に strunicode を混ぜないことで防止できるわけだが、 Google App Engine が提供する機能の中には、どちらの文字列を返すのかドキュメントにはっきりと書かれていない部分もあるので注意した方がいい。例えばフォームの値を取得しようとすると意外にも unicode が返ってくる。

class FrontDesk(webapp.RequestHandler):
    def post(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.out.write(repr(type(self.request.get('value1'))))  # <type 'unicode'>


ちなみに self.response.out の話に戻るけど、 unicode も最終的にブラウザへ送るときは適当な文字コードで符号化されるはずである。そちらはどうなっているかというと、

if isinstance(body, unicode):
    body = body.encode('utf-8')

というコードが webapp フレームワークに埋め込まれているので大丈夫。フォームとは違って、なぜか任意の文字コードを指定できるようになってないんだな。

*1: UnicodeError が発生するかもしれないと StringIO のドキュメントに注意書きがある。

数字をコンマで区切る

いつの間にか数字をコンマで区切る方法がやけに簡単になってるな。

# Python 3.1
print("{:,d}".format(1234567890))  # 1,234,567,890

さらに書式指定を n にすると、ロケールに合わせた桁数と文字で区切ってくれる。例えばドイツでは3桁ごとに . で区切っているようだ。

import locale
locale.setlocale(locale.LC_NUMERIC, "")
print("{:n}".format(1234567890))  # 1,234,567,890

locale.setlocale(locale.LC_NUMERIC, 'deu_deu')
print("{:n}".format(1234567890))  # 1.234.567.890

最初はいらないと思った str.format() もなかなか便利だなあ。

unicodeのraw文字列は \ をエスケープしない場合がある

Vistaに変えたので、ハードコードされたパスを置き換えていたところ、以下のようなコードでエラーになることに気付いた。

path = ur'C:\Users\foo\Documents'

例外はこんな感じ。文法エラーがあると言っている。

SyntaxError: (unicode error) 'rawunicodeescape' codec can't decode bytes in position 2-3: truncated \uXXXX

ドキュメントを見ると、確かに ur を付けた場合 \u\Uエスケープされないと書いてあるな。

r" および "R" 接頭文字を "u" や "U" と合わせて使った場合、\uXXXXおよび \UXXXXXXXX エスケープシーケンスは処理されます

http://www.python.jp/doc/release/ref/strings.html

えー、つまり ur を付けたら \U って書けないってこと? しょうがないので ur'C:\\Users\foo\Documents' と書くようにした*1。区切り文字は複数あっても構わないようなので。

ちなみに、Python3からは、全てエスケープされるようになるとのこと。

*1:普通は os.path.expandvars() を使った方がいい。