Python 的 print 语句有一个很奇怪的 bug。它的功能是向控制台输出字符,这本身不是问题。但是 Python 内部是支持 Unicode 字符串的,而 Unicode 字符串在用 print 输出时 print 要进行一次从 Unicode 到 ANSI/MBCS 编码的编码,编码后才会以 8-bit 流输出结果。
编码就编码吧,这也是很正常的。对于控制台程序来说,输出可能被重定向到文本文件。如果不指定编码,重定向时就不知道以何种 8-bit 字节流写入文本文件,所以,输出到控制台的东西理论上也应该是经过编码的 8-bit 流。综上所述,确实有必要进行一次 WCHAR 到 char 的转码。
但是问题在于,Python 的 print 语句在转码时,居然用的是 strict 规则。即,待输出字符串若含有当前代码页之外的字符,就会在转码过程中出现不可转码的文字,从而抛出 exception。print 语句又不处理这个 exception,导致一个平平常常 print 语句竟然会引起 Python 程序的异常!这简直是不可思议。
比如说你写了这么一段代码:
a = u'测试啊'
print a
然后把控制台切到某个不包含这些汉字的编码页例如 437,输入 chcp 437。然后再运行这段程序,就会看到异常。实际上直接输出到控制台的是另外一种 UnicodeEncodeError 异常,因为控制台设置了代码 页,Python 会试图转码到那个代码页。而更典型的(使开发者发现问题的)异常通常是把输出重定向到文件时,看到的下面这个更典型的异常:
UnicodeEncodeError: 'ascii' codec can't encode character u'\xa1' in position 0-2: ordinal not in range(12
注意,控制台直接输出有异常,重定向输出也会有异常。这两种异常在系统内部具体过程不同,但原理都是一样的。就是 python 遇到了它认为不能把 Unicode 字符编码成 8-bit 流的情况。区别在于,输出到控制台时,python 会试图按照控制台设置的代码页去编码,而重定向时干脆就按 ASCII 编码,那自然是只有128以内的字符才能显示出来。由此可以看出,输出到控制台时产生的异常更隐蔽,因为绝大部分程序员都是在一种编码下编码+开发的,很少有考虑到这方面的情况。在一种编码下开发,写进代码的字符串,以及从文本读出来的字符串,通常也能在这个编码下在控制台输出,从而把问题的发现推迟到了用户(使用了不同代码页)阶段,或是推迟到了重定向输出的时候(因为重定向默认用 ASCII 编码,字符集最小)。知道了原因,会觉得错误可以理解。
说句题外话,令我最不能理解的是,一个好好的 print 语句,输出字符串也不是 zero-terminated,不存在烫烫烫烫过了越到不可访问内存崩溃了的结果,竟然会导致程序异常!首先别跟我说让程序员去控制print 里字符串的内容,这有的时候程序根本控制不了。比如,读出一个文件并显示内容的时候。也别跟我说去 try-except,连 print 都失败了你叫程序员情何以堪啊?看来只能想想办法自己解决这个问题了。
首先要说明的是,既然事关控制台,要做 8-bit 流的输入输出,就没有完美的解决方案。我个人的建议是,在 Windows 下,一切字符串操作,都应该尽可能使用 WCHAR 及相关函数。遇到需要跨平台和网络传输的情况,再使用 UTF-8 编码的 char 字符串。在与古老的 ANSI/MBCS 程序交互时,在严格限制的情况下使用该种编码的 char 字符串。尽管并没有完美的解决方案,在实际情况中,Windows 下 Python 程序也许应该可以有更好的表现。
解决方案一、最简单解决重定向异常的方法是:
import sys
reload(sys)
sys.setdefaultencoding("utf-8")
然后再输出就可以了。直接调 sys.setdefaultencoding() 这个函数是不行的,必须要 reload 一次。具体原因可以参见http://docs.python.org/library/sys.html,我就没有深入研究了。
这个不会影响控制台直接输出,只会影响重定向,所以最好是写 utf-8 反正连 Windows 的记事本都可以打开 UTF-8 的文本。当然这么做也有不足,就是如果某一个程序,调用了你写的 Python 程序,把输出重定向到它的窗口里,这时这个程序很可能是按系统默认编码去解码的,用户就看到一片乱码了。这个没什么好办法,要么外围程序做好点可以设置控制台解码,要么你就只能获取一下当前控制台编码设置(不知道 Python 里有没有好方法,我可以用 Windows API 做到),当然这样的话就无法防止异常了……
解决方案二、用 print a.encode("gbk", "replace") 取代 print a:
对控制台来说,由于输出的是字节流,所以具体显示成什么字符,取决于控制台的代码页设置。输出重定向也是一样,取决于你打开文件的方式。如果打开文件发现乱码了,那你要说:一定是我打开的方式不对!
这个方案好处在于可以让程序完全像使用了 Windows ANSI 函数的程序那样工作。输入、输出全都是按某个特定编码来做的,仿佛程序内部固化的字符串就是按某个特定编码写的。不过,程序里有几千个 print 就得换几千次就不说了,万一你换漏了,又要出悲剧。
当然,既然完全像一个 Windows ANSI 程序的行为,那么不可避免的问题就是乱码。假设你所有字符串都按 GBK 在输入输出时编码了,那如果用户设置的控制台代码页根本就不是 GBK 呢?又乱码了不是……而且既然我输入输出都是 GBK,干嘛程序内部还要用 Unicode 呢?大概就只是为了防止内部处理时即出现异常吧。
最关键的是这实在不是一个程序员的作风。就没有自动化一点的方案吗?
解决方案三、更改 sys.stdout 的编码:
既然问题出在 sys.stdout 的编码往往不能满足字符集需求上,为什么不直接更改它的编码呢?http://www.doughellmann.com/PyMOTW/codecs/ 提供了一种方案:
import sys, codecs
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
这 个方案的好处就是它同时影响控制台直接输出和重定向输出,比方案一强,已经达到了方案二的水平。不过它面临一个方案二没有而方案一还有的问题,就是如果设 置的不是 "utf-8",那么就有可能出 UnicodeEncodeError。如果设置的是 "utf-8",那就要面临配套设施不完善而看到的乱码问题。
最要命的是,其实你是根本无法在控制台设置成 cp65001 的情况下让程序正常运行的!这是方案二也会同样遇到的问题。假设我们设置了 utf-8,要想在控制台正常阅读输出结果,那也就要把控制台用 chcp 65001 设置成 UTF-8。但是,设置之后,python 会以为当前代码页叫 "cp65001",不认,会出这个错误:
LookupError: unknown encoding: cp65001
呃,好吧,这也是有办法可以解决的,出自 http://stackoverflow.com/questions/878972/windows-cmd-encoding-change-causes-python-crash:
import codecs
codecs.register(lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
这样 Python 就认 "cp65001" 这个东西就是 "utf-8" 的别名了。这样,你就可以在控制台 chcp 65001 然后看到输出字符了。不过遗憾的是,这只是理论上的。实际上如果你 print a 的时候第一个字符不是纯 ASCII 的,即 Unicode 码在 128 以上,根本无法正常显示。我们不妨把前面学到的知识都拼起来,写一段代码,期望它能正常工作吧:
#coding=utf-8
a = u'测试啊'
import sys
reload(sys)
sys.setdefaultencoding("utf-8")
import codecs
codecs.register(lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
print a.encode("utf-8", "replace")
实际上运行结果是:
���试啊Traceback (most recent call last):
File "C:\Python25\Test1.py", line 11, in
print a.encode("utf-8", "replace")
IOError: [Errno 2] No such file or directory
这莫名其妙的 IOError 是怎么回事?而且字符串第一个字符也无法正常显示,会变成若干个“�”。该字符在 UTF-8 中是几个字节,就有几个“�”字符。我™想破了脑袋也想不出 Python 是怎么写出这样的 bug 来的!注意,不是说第一个字符是纯 ASCII 就可以了,只是那样做的话输出来的异常信息是可以看,但是异常还是有的。如果是用 sys.stdout = codecs.getwriter() 法直接 print a 的话,出现的错误是:
���试啊Traceback (most recent call last):
File "C:\Python25\Test1.py", line 13, in
print a
File "C:\Python25\lib\codecs.py", line 304, in write
self.stream.write(data)
IOError: [Errno 0] Error
所以实际上是根本没法用的。我测试的版本是 Python 2.5.2,不知道后续版本是否有改进。
而且还有一个问题是如果你 chcp 65001 之后,打过一些汉字或者用 type 显示过文件,就会发现怎么光标的位置都不对啊!换行也不对啊喂后面怎么好多东西超出去了看不到啊!
没错恭喜你遇到了最头疼的问题!在 cp65001 下,并不像那些中国、日本、韩国的代码页下面那样区分全角和半角,所有的字符在计算光标的时候都占同样的宽度,但是字体渲染仍然正常。也就是说,如果(假 设一行设置的是 80 个字符)你在一行里写了 80 个汉字,那么前 40 个渲染的时候就已经把整行占满了,可是没有自动换行,自动换行要到 80 列才有,所以后 40 个汉字就看不见了。
坑爹呀。
遗憾的是这还根本没有解决办法。要想让全角字符正确地占两个半角字符的宽度,就只能用一些支持这个特性的代码页,比如 cp936,就是 GBK。当然,这样就不能显示全部 Unicode 字符了,万一有用户输入了这个,就只能被替换成 ? 或者其它什么东西了。
所以说,只要还跟该死的 char 字节流打交道,跟 stdout 打交道,就没法有一个完美方案。
解决方案四、彻底不使用stdout:
这堆乱七八糟的事情从根本上来说是因为控制台的 stdout 只能接受 8-bit 字节流,也就是 char,所以才有了这么多有的没的编码问题。如果能够让 python 在用 print 的时候底层使用一个接受 WCHAR 的函数来做事,也许事情就有很大转机。
事实上,还是在 http://stackoverflow.com/questions/5109970/linux-python-encoding-a-unicode-string-for-print 就有一篇终极解决方案。它用接受 WCHAR 的 Windows API 做控制台输出,而同时把重定向交由原有方式处理,在兼顾重定向的情况下,实现了控制台下最完美的输出方案。
首先请看代码:
import sys
if sys.platform == "win32":
import codecs
from ctypes import WINFUNCTYPE, windll, POINTER, byref, c_int
from ctypes.wintypes import BOOL, HANDLE, DWORD, LPWSTR, LPCWSTR, LPVOID
original_stderr = sys.stderr
# If any exception occurs in this code, we'll probably try to print it on stderr,
# which makes for frustrating debugging if stderr is directed to our wrapper.
# So be paranoid about catching errors and reporting them to original_stderr,
# so that we can at least see them.
def _complain(message):
print >>original_stderr, isinstance(message, str) and message or repr(message)
# Work around <http://bugs.python.org/issue6058>.
codecs.register(lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
# Make Unicode console output work independently of the current code page.
# This also fixes <http://bugs.python.org/issue1602>.
# Credit to Michael Kaplan <http://blogs.msdn.com/b/michkap/archive/2010/04/07/9989346.aspx>
# and TZOmegaTZIOY
# <http://stackoverflow.com/questions/878972/windows-cmd-encoding-change-causes-python-crash/1432462#1432462>.
try:
# <http://msdn.microsoft.com/en-us/library/ms683231(VS.85).aspx>
# HANDLE WINAPI GetStdHandle(DWORD nStdHandle);
# returns INVALID_HANDLE_VALUE, NULL, or a valid handle
#
# <http://msdn.microsoft.com/en-us/library/aa364960(VS.85).aspx>
# DWORD WINAPI GetFileType(DWORD hFile);
#
# <http://msdn.microsoft.com/en-us/library/ms683167(VS.85).aspx>
# BOOL WINAPI GetConsoleMode(HANDLE hConsole, LPDWORD lpMode);
GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(("GetStdHandle", windll.kernel32))
STD_OUTPUT_HANDLE = DWORD(-11)
STD_ERROR_HANDLE = DWORD(-12)
GetFileType = WINFUNCTYPE(DWORD, DWORD)(("GetFileType", windll.kernel32))
FILE_TYPE_CHAR = 0x0002
FILE_TYPE_REMOTE = 0x8000
GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD)) \
(("GetConsoleMode", windll.kernel32))
INVALID_HANDLE_VALUE = DWORD(-1).value
def not_a_console(handle):
if handle == INVALID_HANDLE_VALUE or handle is None:
return True
return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR
or GetConsoleMode(handle, byref(DWORD())) == 0)
old_stdout_fileno = None
old_stderr_fileno = None
if hasattr(sys.stdout, 'fileno'):
old_stdout_fileno = sys.stdout.fileno()
if hasattr(sys.stderr, 'fileno'):
old_stderr_fileno = sys.stderr.fileno()
STDOUT_FILENO = 1
STDERR_FILENO = 2
real_stdout = (old_stdout_fileno == STDOUT_FILENO)
real_stderr = (old_stderr_fileno == STDERR_FILENO)
if real_stdout:
hStdout = GetStdHandle(STD_OUTPUT_HANDLE)
if not_a_console(hStdout):
real_stdout = False
if real_stderr:
hStderr = GetStdHandle(STD_ERROR_HANDLE)
if not_a_console(hStderr):
real_stderr = False
if real_stdout or real_stderr:
# BOOL WINAPI WriteConsoleW(HANDLE hOutput, LPWSTR lpBuffer, DWORD nChars,
# LPDWORD lpCharsWritten, LPVOID lpReserved);
WriteConsoleW = WINFUNCTYPE(BOOL, HANDLE, LPWSTR, DWORD, POINTER(DWORD), \
LPVOID)(("WriteConsoleW", windll.kernel32))
class UnicodeOutput:
def __init__(self, hConsole, stream, fileno, name):
self._hConsole = hConsole
self._stream = stream
self._fileno = fileno
self.closed = False
self.softspace = False
self.mode = 'w'
self.encoding = 'utf-8'
self.name = name
self.flush()
def isatty(self):
return False
def close(self):
# don't really close the handle, that would only cause problems
self.closed = True
def fileno(self):
return self._fileno
def flush(self):
if self._hConsole is None:
try:
self._stream.flush()
except Exception, e:
_complain("%s.flush: %r from %r"
% (self.name, e, self._stream))
raise
def write(self, text):
try:
if self._hConsole is None:
if isinstance(text, unicode):
text = text.encode('utf-8')
self._stream.write(text)
else:
if not isinstance(text, unicode):
text = str(text).decode('utf-8')
remaining = len(text)
while remaining > 0:
n = DWORD(0)
# There is a shorter-than-documented limitation on the
# length of the string passed to WriteConsoleW (see
# <http://tahoe-lafs.org/trac/tahoe-lafs/ticket/1232>.
retval = WriteConsoleW(self._hConsole, text,
min(remaining, 10000),
byref(n), None)
if retval == 0 or n.value == 0:
raise IOError("WriteConsoleW returned %r, n.value = %r"
% (retval, n.value))
remaining -= n.value
if remaining == 0: break
text = text[n.value:]
except Exception, e:
_complain("%s.write: %r" % (self.name, e))
raise
def writelines(self, lines):
try:
for line in lines:
self.write(line)
except Exception, e:
_complain("%s.writelines: %r" % (self.name, e))
raise
if real_stdout:
sys.stdout = UnicodeOutput(hStdout, None, STDOUT_FILENO,
'<Unicode console stdout>')
else:
sys.stdout = UnicodeOutput(None, sys.stdout, old_stdout_fileno,
'<Unicode redirected stdout>')
if real_stderr:
sys.stderr = UnicodeOutput(hStderr, None, STDERR_FILENO,
'<Unicode console stderr>')
else:
sys.stderr = UnicodeOutput(None, sys.stderr, old_stderr_fileno,
'<Unicode redirected stderr>')
except Exception, e:
_complain("exception %r while fixing up sys.stdout and sys.stderr" % (e,))
# While we're at it, let's unmangle the command-line arguments:
# This works around <http://bugs.python.org/issue2128>.
GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int)) \
(("CommandLineToArgvW", windll.shell32))
argc = c_int(0)
argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc))
argv = [argv_unicode[i].encode('utf-8') for i in xrange(0, argc.value)]
if not hasattr(sys, 'frozen'):
# If this is an executable produced by py2exe or bbfreeze, then it will
# have been invoked directly. Otherwise, unicode_argv[0] is the Python
# interpreter, so skip that.
argv = argv[1:]
# Also skip option arguments to the Python interpreter.
while len(argv) > 0:
arg = argv[0]
if not arg.startswith(u"-") or arg == u"-":
break
argv = argv[1:]
if arg == u'-m':
# sys.argv[0] should really be the absolute path of the module source,
# but never mind
break
if arg == u'-c':
argv[0] = u'-c'
break
# if you like:
sys.argv = argv
简单来说这段代码做了这么几个事:
1、如果输出到控制台,改用 WriteConsoleW()。
2、如果输出被重定向,用 utf-8 编码输出。
3、用 GetCommandLineW() 和 CommandLineToArgvW() 获取命令行参数,在最后一行取代 sys.argv 传入的参数。
这个是我目前能找到的最完美的解决方案了。在控制台下也能不出错,在重定向的时候也可以按 UTF-8 去编码成 char 字节流。唯一的问题是 Python 2.5.2 里似乎没有 LPVOID。我用 c_void_p 取代 LPVOID,似乎是可行的。
当然,它仍然有前述不可避免的问题。例如在非原生支持汉字的代码页(简 936 繁 950 日 932 韩 949)下,光标和换行的位置会出问题。如 果对汉字显示有很高的要求,不妨调用 Windows API 设置一下控制台的代码页。此外,输出重定向到外围程序时,如果外围程序不能设置按 UTF-8 解码,就会看到乱码的问题也依然存在。这些问题,就留待读者自行解决吧。
最后,特别说明一下以上问题都是 Windows 平台限定的。Linux 下问题没有这么显著(现在的Linux发行版本多数都设置了默认代码页为 UTF-8),而且就算用户代码页不是 UTF-8,也没有 Windows 下 WriteConsoleW 这么淫霸的函数,所以洗洗睡吧。





小悦悦事件之我见
刚才看了一篇文章,题目《小悦悦事件的44个疑点, 某些无良媒体用鲜血制造的“新闻”》,觉得作者分析得挺在理的。有些感触,不吐不快。
我记得我刚到美国那会儿,也是哪个非洲国家,在万恶的美帝的煽动下,国内搞了革命。那时候有些人在转发一个视频,说是非洲的某独裁者多么多么可恨,竟然枪杀了这么可爱的一个女孩。我就点进视频进去看了一下。不看不知道,看了才发现美国民众的智商,确实没有下限。
视频明显是一个手机拍摄的,有摇晃,看来看去。画面的远处是示威的人群,而摄影者远离人群。突然,画面里走进一个年轻女生,手机就跟着她拍。拍摄者远离示威人群,显然女生也是远离示威人群的,而且看都没有看那边一眼。也许是怕惹麻烦,所以躲着走吧。可是,就在她走过镜头前的时候,突然仰面朝天倒在地上。一发从远处射来的子弹,准确地穿过了她的心脏。少女倒在地上,鲜血从口中不断涌出,身上的伤口也染红了胸前的衣服。她的眼睛还睁着,仿佛还在纳闷自己为什么会被击中。
我也很纳闷。明明示威者就在视频的远方,而女孩是从画面一侧走到另一侧的。不论是示威者打向远方的子弹,还是政府军还击的子弹, 都应该是平行于摄像机视角方向的,不应该从侧面打穿这个女孩。我能得出的唯一结论,就是这是无良的西方媒体,为了给背后的西方势力造势,煽动非洲小国闹革命,而打的一出舆论战。你看到的一切,都是一向标榜公正无私的西方媒体,为了他们一向标榜民主自由的主子,而安排的一场好戏。除了那个女生。
也许她不相信西方人会带来美好的生活,这样起码她还用自己的生命验证了西方帝国主义列强的虚伪。如果她像我们国家的一些民逗、带路党一样相信西方势力的介入是会给自己带来美丽新生活的,那她就真的是一个悲剧了。可能她至死都没有搞明白,自己怎么就成了西方势力侵略祖国过程中的一枚炮灰。不过,这并不是她的错。这样的编排就像是一场无差别杀人游戏,导演这出戏的人,只需要一个年轻、漂亮,容易激发观众同情心的姑娘走过自己的镜头前,杀死毫无防备的她,一出好戏就拍完了。经过那里的任何人,都可能成为枪下的冤魂。
至于后来这个非洲小国的革命进行得怎么样了,我是没有关注。非洲国家名字都不好记,过了几天也就忘了。不过看看利比亚就知道。据说法国总统几年前还赞扬卡扎菲把利比亚带向了繁荣富强,让利比亚人民过上了好日子。结果没几年,法国倒戈一击,带头轰炸利比亚。每当看到国际社会上这些外国政客翻云覆雨的丑恶嘴脸,我都十分庆幸毛主席当年让全国人民勒紧裤腰带也要搞出核武器来。吃几年苦搞出来了,以后再怎么发展,都不怕外国势力打压,人民真正能有个盼头。要是一开始就让人民过上悠闲安逸的日子,那肯定是稍微发展一点,世界老大就要来阻止你了。这就像高半夜凉初透考前拼命备考一样,苦一时,安逸一世。
别跟我说美国不打法国不打英国是因为英法联军是民主国家,根本原因是两点:一、英法不跟美国拧着干,美国说啥就是啥;二、欧洲的经济再怎么干也干不过美国了,不然就是不听话。现在有可能超越美国的国家就中国一个,为什么?硬实力上能抗衡, 经济上才有可能发展。经济基础决定上层建筑,那又是什么决定经济基础呢?是军事实力。没有军事实力,守不住财,当然也就别想发财。发一点人家都会来抢的。
咱还是说回小悦悦这个事件。我觉得,这个事儿之所以蹊跷,还跟它发生的时机有关系。最近想必关心实事的人也听说了,一帮民知民逗正因为一个盲人的事儿而跟政府对着干呢。如果你说中央政府想在这个时候制造一点社会事件转移视线,确实是太可能了。就搞新闻的那帮唯恐天下不乱的人,什么时候这么好心出来表扬好人好事了?在我印象里弘扬正气那一贯是CCTV干的事儿,各种小报,尤其是南方XX报,最喜欢挑社会阴暗面报道。让人觉得过不下去了,思想阴暗了,这报社还要自命不凡,说我是大胆揭露歪门邪道。我说你揭露可以,但不光要惩恶,还要扬善。最近某些媒体老拿医患纠纷说事儿,这就是心里烂到根儿上了。这个是后话,暂且不表。
所以我就想,要是有什么组织,能让全国媒体这么一根筋地赞扬真善美,那可能还真是中央政府有这个本事了。所以很可能是中央想转移视线,找了广东的媒体,让他们搞一个大事件出来。不仅要轰动,还要能引发大讨论。最好是能让美分党和五毛党对掐的,最好是能让共产党和社会主义躺着也中枪的,这样最能把自己择出去。结果媒体一听,很高兴(或者“很无奈”,按某些媒体人一贯的说法),就出去找啊找,终于找到了原文中提到的这么一对夫妇。可能真的是生了个女儿不想要,想再生个儿子。三方面一拍即合,才有了今天你们看到的这出戏。
当然,还是得说在造假这种事儿上我们国人比老美聪明多了。起码车是从路上开过来的,没有从店铺里冲出来。这比射在非洲少女胸口的那一枪要真实得多。可是还是有很多很多的问题,能被原文作者这样细心的人挑出来。毕竟,老百姓没有影帝的本事。至于那位拾荒大妈,我觉得真不是演员。可能她只是被算准了,在那一天那一时刻会在这条路上经过。可能也没计算得这么精确,说不定不管是谁,从那里路过了,只要不是演员,就会去扶一把,救一把,让这出没有胜利者的戏走向封镜。
但不管怎么说,这样的手段都太罪恶了。我觉得中央的本意是好的,只是媒体无良,造了这么没下限的事儿出来。最后诚挚地祝愿天下无良的新闻人全家死光光,永世不得超生。
No Comments »