chcp 65001 的代码页会导致程序在没有任何错误的情况下终止。

5

问题

当我想在Python解释器中输入Unicode字符时(为了简单起见,我在示例中使用了a-umlaut,但我最初遇到的是波斯语字符),问题就出现了。每当我使用CHCP 65001代码页运行Python,然后尝试输入一个Unicode字符时,Python就会退出而没有任何错误。

我花了几天时间试图解决这个问题,但都没有成功。但今天,我在Python网站上找到了一个帖子,另一个在MySQL上,还有一个在Lua-users上,其中提出了关于这种突然退出的问题,尽管没有任何解决方案,一些人说chcp 65001本质上是有问题的。

彻底弄清楚这个问题是与chcp设计相关还是存在可能的解决方法将是很好的。

重现错误

chcp 65001

Python 3.X:

Python shell

print('ä')

结果:它只是退出了shell

然而,这个可以工作:python.exe -c "print('ä')" 还有这个:print('\u00e4')

结果:ä

在Luajit2.0.4中

print('ä')

结果:它只是退出了shell

然而这个可以工作:print('\xc3\xa4')

到目前为止,我得出了以下观察结果:

  1. 使用命令提示符直接输出可以工作。
  2. 基于Unicode、基于十六进制的字符等价物可以工作。

所以

这不是Python的错误,我们不能直接在Windows命令提示符或其任何包装器(如ConEmuCmder)中使用Unicode字符。我正在使用Cmder来在Windows shell中查看和使用Unicode字符,并且没有遇到任何问题。这样正确吗?


我安装了多个Python版本。我无法在Windows 10 64位上重现,使用的是Python 3.3.5 64位或Python 3.5.2 64位,但是可以使用Python 2.7.12 32位。它按照描述退出,但是你说你正在使用Python 3。也许这是32位与64位之间的问题?你是在使用Windows cmd.exe控制台还是其他什么? - Mark Tolonen
@MarkTolonen,当使用控制台(conhost;cmd只是一个shell)时,在所有Windows版本中都可以重现此问题,该控制台并未设计用于代码页65001。输入单个非ASCII字符会导致空读取,Python的REPL和input将其处理为EOF。问题在于conhost.exe假定它正在将其UTF-16输入缓冲区编码为具有每个字符1个字节的ANSI代码页,因此对于非ASCII UTF-8,其WideCharToMultiByte编码缓冲区太小。读取失败,但它被返回给客户端作为0字节的“成功”读取,即文件结束。 - Eryk Sun
@eryksun,是的,我知道它已经损坏了,但是在64位Windows 10上的64位Python中,我可以使用国际IME在cmd.exe中键入print('ä')并且它可以正确地打印出来。因此,“在所有Windows版本中都能重现”这个说法是不准确的,至少对于这个特定的例子而言。 - Mark Tolonen
@MarkTolonen,你安装了pyreadline吗? - Eryk Sun
顺便说一句,@eryksun你的解释回答了我的问题。非常感谢。 - psychob
显示剩余3条评论
1个回答

14

要在 Python 2.7 和 3.x(3.6 之前)的 Windows 控制台中使用 Unicode,请安装并启用 win_unicode_console。它使用宽字符函数 ReadConsoleWWriteConsoleW,就像其他支持 Unicode 的控制台程序(如 cmd.exe 和 powershell.exe)一样。对于Python 3.6,新添加了一个 io._WindowsConsoleIO 原始 I/O 类。它读取和写入 UTF-8 编码文本(为了与 Unix — "get a byte" — 程序实现跨平台兼容性),但在内部使用宽字符 API 进行转换,可以实现 UTF-16LE 的编码和解码。

您在控制台中遇到的非 ASCII 输入问题在所有 Windows 版本(包括 Windows 10)的控制台中都会出现。控制台主机进程,即 conhost.exe,不支持 UTF-8 (codepage 65001),且没有得到一致的更新支持。特别是,非 ASCII 输入会导致空读取。这反过来会导致 Python 的 REPL 退出,并且内置的 input 函数引发 EOFError 异常。

问题在于 conhost 对其 UTF-16 输入缓冲进行编码时,假定使用单字节代码页(如西方区域的 OEM 和 ANSI 代码页,例如 437、850、1252)。UTF-8 是一种多字节编码,在其中非 ASCII 字符被编码为 2 到 4 个字节。要处理 UTF-8,它需要对 M / 4 个字符进行多次迭代编码,其中 M 是从 N 字节缓冲区中剩余的字节数。相反,它假定读取 N 字节是请求读取 N 个字符。然后,如果输入包含一个或多个非 ASCII 字符,由于缓冲区过小,内部的 WideCharToMultiByte 调用将失败,并且控制台会返回读取了 0 字节的“成功”读取。
如果安装了 pyreadline 模块,则在 Python 3.5 中可能不会完全观察到这个问题。Python 3.5 自动尝试导入 readline。在 pyreadline 的情况下,通过宽字符函数 ReadConsoleInputW 读取输入。这是一个低级函数,用于读取控制台输入记录。原则上应该可以工作,但实际上输入 print('ä') 在 REPL 中被读取为 print('')。对于非 ASCII 字符,ReadConsoleInputW 返回一系列 Alt+Numpad 的 KEY_EVENT 记录。该序列是有损的 OEM 编码,可以忽略除最后一个记录以外的所有记录,该记录在 UnicodeChar 字段中包含输入字符。显然,pyreadline 忽略了整个序列。在Windows 8之前,使用代码页65001进行输出也存在问题。它会打印出一堆垃圾文本,其数量与非ASCII字符的数量成比例。在这种情况下,问题在于WriteFileWriteConsoleA返回的是写入屏幕缓冲区的UTF-16代码的数量,而不是UTF-8字节的数量。这会使Python的缓冲写入程序感到困惑,导致它重复写入其认为未写入的剩余字节。在Windows 8中,这个问题已经得到了解决,因为重写了内部控制台API并使用ConDrv设备代替LPC端口。旧版的Windows可以使用ConEmu或ANSICON来解决这个bug。

您的描述非常有帮助,但其中一部分是错误的:输入“异国字符”(即不在当前键盘布局中的字符)。我在以下链接中写下了我所知道的内容:(https://dev59.com/3XRC5IYBdhLWcg3wK9yV#47843552) 和 (https://dev59.com/rZjga4cB1Zd3GeqPFRvT#47852866)。 - Ilya Zakharevich
@IlyaZakharevich,我读了你的帖子,但我仍然不完全确定我的回答有什么问题,除非它只是我的表述方式有误,将“非ASCII”输入描述为与当前键盘映射中特定可用的内容不同。 - Eryk Sun
对于非ASCII字符,“ReadConsoleInputW”返回一系列Alt+Numpad“KEY_EVENT”记录。这是错误的(或者至少不完全正确的;)。所描述的行为仅发生在键盘“主平面”中不存在的字符上。如果一个字符可以通过(无论多么复杂的)修饰符组合访问(但不需要前缀按键!),它将被伪造为以这种方式输入。 - Ilya Zakharevich
应用程序将检测到某个带有特定修饰键的按键(如果我没记错的话,最多可以模拟 Shift-AltGr 修饰键;如果需要任何扩展修饰键[包括 Ctrl],则会替换为 AltGr)。 - Ilya Zakharevich
@IlyaZakharevich,谢谢。这就是我认为你的意思。我会研究一下控制台在这里的操作。之前我只是尝试了一些随机的非ASCII字符,并观察到它们使用Alt+Numpad序列来适应最佳的OEM编码,实际的Unicode代码点存储在最后一个记录中。 - Eryk Sun

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接