如果有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.cint
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,所以還是避免用這樣的方式比較好。
延伸閱讀
沒有留言:
張貼留言