コーデック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') # 简体字
ここまで来て気付いたのだが、コードページが 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') # 简体字
問題なし。
Python 3.x の winreg では、ここに書いたような問題は起きないはず。UNICODE 版の Windows API を使っているからね。早く 3.x に移行したいなあ。
参考
関連記事
レジストリの用語が統一されていない
レジストリについて調べていたら、文書毎に用語が統一されていないことに気付いた。
とりあえず見つけたものを表にしてみた。
自分が使っている用語 | 別名 |
---|---|
ルートキー | メインキー、定義済みキー |
値 | 値エントリ、レジストリエントリ |
既定値 | 標準の値、既定の値、名前なしのレジストリエントリ、標準のレジストリエントリ、既定のエントリ、名前なしの値、デフォルトの値 |
値の名前 | レジストリエントリ名、エントリの名前 |
値の種類 | 値型、値の型、値のタイプ、レジストリエントリのデータ型 |
値のデータ | 値、設定値、エントリの値、エントリ値、レジストリエントリのデータ |
値のことをレジストリエントリと呼ぶのは一般的みたい。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() # ここでエラー
これは単純に str と unicode を混ぜないことで防止できるわけだが、 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() を使った方がいい。