如果有hook stdout的需求的話,通常會把sys.stdout指向自行定義的objecct身上,以下這個範例是實作一個HookStdOut,將output string重新導向stdout與debug message
import sys class HookStdOut(object): def __init__(self, *argv, **argd): object.__init__(self) def write(self, s): from ctypes import windll if isinstance(s, unicode): windll.kernel32.OutputDebugStringW(s) else: windll.kernel32.OutputDebugStringA(s) sys.__stdout__.write(s) sys.stdout = HookStdOut() s = unichr(0x5927) + unichr(0x5BB6) + unichr(0x597D) #in cp950 (大, 家, 好) try: print s except: import traceback traceback.print_exc()
但是這個方法如果在print unicode時,就會有exception出現 UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-2: ordinal not in range(128)
這是因為Python在print unicode到stdout前,會試著把unicode以console code page做encode的動作,再write到stdout。而現在stdout是我們自行定義的HookStdOut,最後write到stdout時,只會以ascii的方式encode,然後就會出現UnicodeEncodeError了。
研究了一下Python 2.5的source code...
Python\sysmodule.c
PyObject * _PySys_Init(void) { //... sysout = PyFile_FromFile(stdout, "<stdout>", "w", _check_and_flush); //... if(isatty(_fileno(stdout)) && PyFile_Check(sysout)) { sprintf(buf, "cp%d", GetConsoleOutputCP()); if (!PyFile_SetEncoding(sysout, buf)) return NULL; } //... }Objects\fileobject.c
int PyFile_WriteObject(PyObject *v, PyObject *f, int flags) { PyObject *writer, *value, *args, *result; if (f == NULL) { PyErr_SetString(PyExc_TypeError, "writeobject with NULL file"); return -1; } else if (PyFile_Check(f)) { FILE *fp = PyFile_AsFile(f); #ifdef Py_USING_UNICODE PyObject *enc = ((PyFileObject*)f)->f_encoding; int result; #endif if (fp == NULL) { err_closed(); return -1; } #ifdef Py_USING_UNICODE if ((flags & Py_PRINT_RAW) && PyUnicode_Check(v) && enc != Py_None) { char *cenc = PyString_AS_STRING(enc); value = PyUnicode_AsEncodedString(v, cenc, "strict"); if (value == NULL) return -1; } else { value = v; Py_INCREF(value); } result = PyObject_Print(value, fp, flags); Py_DECREF(value); return result; #else return PyObject_Print(v, fp, flags); #endif } //... }在Python default的sys.stdout,其實是一個FileObject,會以Windows API GetConsoleCP()將console code page 記錄在f_encoding這個變數裡,等到print時會call進PyFile_WriteObject(),如果要print的PyObject *v是PyUnicode的話,會先將PyUnicode以f_encoding做encode,然後再print出去。
而我們實作了一個HookStdOut,但是我們並沒有指定它的encoding是什麼,所以default是ascii,在嘗試print unicode時,就會出現exception了。所以,我們必須要在write()裡自己encode到正確的code page才不會有這個exception出現。
正確版的範例:
import sys class HookStdOut(object): def __init__(self, *argv, **argd): object.__init__(self) self.encoding = sys.getfilesystemencoding() def write(self, s): from ctypes import windll if isinstance(s, unicode): windll.kernel32.OutputDebugStringW(s) sys.__stdout__.write(s.encode(self.encoding)) #try to encode the unicode to default file system encoding else: windll.kernel32.OutputDebugStringA(s) sys.__stdout__.write(s) sys.stdout = HookStdOut() s = unichr(0x5927) + unichr(0x5BB6) + unichr(0x597D) #in cp950 (大, 家, 好) try: print s except: import traceback traceback.print_exc()
其實還有另一個方法可以避掉UnicodeEncodeError的exception,但是並不建議這麼做。
class HookStdOut(object): def __init__(self, *argv, **argd): object.__init__(self) def write(self, s): from ctypes import windll if isinstance(s, unicode): windll.kernel32.OutputDebugStringW(s) else: windll.kernel32.OutputDebugStringA(s) sys.__stdout__.write(s) #reset default encoding!!! reload(sys) sys.setdefaultencoding(sys.getfilesystemencoding()) sys.stdout = HookStdOut() s = unichr(0x5927) + unichr(0x5BB6) + unichr(0x597D) #in cp950 (大, 家, 好) try: print s except: import traceback traceback.print_exc()由於在Python2.5的site.py將sys.setdefaultencoding remove掉了,所以必須要reload sys module,然後再setdefaultencoding一次才行。
但是參考一下這篇文章,setdefaultencoding is evil,我想,在小程式裡可能可以用setdefaultencoding的方式解掉,但如果是大型的project的話,並非所有的unicode會用相同的encoding,所以還是避免用這樣的方式比較好。
延伸閱讀
沒有留言:
張貼留言