为什么脚本语言在Windows控制台中不输出Unicode?

19
Windows控制台至少在过去十年中已经支持Unicode,可能还可以追溯到Windows NT。然而出于某种原因,包括Perl和Python在内的主要跨平台脚本语言仅输出各种8位编码,需要大量麻烦才能解决。Perl会发出“在打印中宽字符”的警告,Python会给出charmap错误并退出。这么多年过去了,为什么它们不只是简单地调用Win32 -W API以输出UTF-16 Unicode,而是强制将所有内容通过ANSI/codepage瓶颈呢?
这只是因为跨平台性能不够重要吗?是因为这些语言在内部使用UTF-8,并发现输出UTF-16太麻烦了吗?还是-W API本质上有如此严重的缺陷,无法直接使用?
更新
看起来责任可能需要由所有各方共同承担。我想象中脚本语言可以在Windows上调用wprintf,让操作系统/运行时处理诸如重定向之类的事情。但事实证明,即使在Windows上,甚至wprintf也会在打印到控制台之前将宽字符转换为ANSI,然后再转换回来
如果此问题已得到解决,请告知我,因为错误报告链接似乎已损坏,但我的Visual C测试代码仍无法通过wprintf,WriteConsoleW则可以。
更新2
实际上,您可以使用_setmode(_fileno(stdout), _O_U16TEXT)在C中将UTF-16打印到控制台,但前提是您必须这样做。

您可以在代码页设置为65001的控制台中从C打印UTF-8,但Perl、Python、PHP和Ruby都存在错误,阻止了这一点。Perl和PHP通过在至少包含一个宽字符的行后添加额外的空行来破坏输出。Ruby有稍微不同的破坏输出。Python会崩溃。

更新3

Node.js是第一个没有这个问题的脚本语言,直接使用即可。

Python开发团队慢慢意识到这是一个真正的问题,因为它最初于2007年底首次报告,并在2016年看到了大量活动,以完全理解和修复该错误。


5
无法“输出Unicode”。 Unicode是一种将字符内部表示为代码点的方法。要输出它,您需要某种形式的编码 - 可能是UTF-8。 - Daniel Roseman
2
当然可以输出Unicode。在*nix中,输出Unicode的标准编码是UTF-8。在Windows中,输出的标准方式是UTF-16,除了在Windows世界中,他们说“Unicode”时指的是UTF-16。这可能也适用于Java和任何其他主要不使用UTF-8的地方。 - hippietrail
5
如果您不喜欢这个术语,可以用“在适当的条件下(如字体支持等),在控制台上打印任意Unicode字符”来代替。UTF-8是Unicode标准的一部分,它做的事情远不止分配代码点。 - Philipp
@Daniel:Unicode 有特定的术语,其中“编码”确切地意味着“将字符表示为代码点的方法”。与此相比,UTF 代表“Unicode 转换格式”,它是将代码点表示为字节流或字等的过程。在 Unicode 世界之外,将字符映射到数字(代码点)并将一系列代码点转换为字节或字串被混淆在一起称为“编码”。这可能会令人困惑和烦恼,但事实就是如此。 - hippietrail
Node.js是我发现的第一种脚本语言,可以在*nix和Windows系统的控制台中直接使用Unicode!当然,它并不是一个常规的脚本语言,而是用于服务器端基于Node的东西,因此许多你从脚本语言中期望的功能都没有。(例如,逐行阅读文本并不容易。) - hippietrail
9个回答

20
主要问题似乎是在Windows上仅使用标准C库和没有平台相关或第三方扩展无法使用Unicode。您提到的语言源自Unix平台,它们实现Unicode的方法与C混合得很好(它们使用普通的char*字符串、C本地函数和UTF-8)。如果想在C中使用Unicode,您不得不写两次代码:一次使用非标准Microsoft扩展,一次使用标准C API函数用于所有其他操作系统。虽然这可以做到,但通常不会给予高优先级,因为这很麻烦,而大多数脚本语言开发人员也不喜欢或忽略Windows。
更技术层面上,我认为大多数标准库设计者的基本假设是所有I/O流在操作系统级别上都是基于字节的,这对于所有操作系统上的文件以及类Unix系统上的所有流都是真实的,只有Windows控制台是例外。因此,许多类库和编程语言标准的架构必须进行大幅修改,如果想要纳入Windows控制台I/O。
另一个更主观的观点是,微软没有尽力推广Unicode的使用。第一个具有不错(当时)Unicode支持的Windows操作系统是1993年发布的Windows NT 3.1,比Linux和OS X支持Unicode的时间要早得多。尽管如此,这些操作系统向Unicode的转换却更加无缝和不会出现问题。微软再次听取了销售人员的意见而非工程师的意见,并将技术上过时的Windows 9x保留到2001年;他们仍然发布了损坏的、现在已经不再需要的8位API接口,并邀请程序员使用它(看看Stack Overflow上最近的一些Windows API问题,大多数新手仍然使用可怕的遗留接口!)。当Unicode问世时,很多人认为它很有用。Unicode最初是一个纯16位编码,因此使用16位代码单元是很自然的。然后微软显然说:“好的,我们有了这个16位编码,所以我们必须创建一个16位API”,没有意识到没有人会使用它。然而,Unix的权威人士认为,“我们如何以一种高效且向后兼容的方式将其整合到当前系统中,以便人们真正使用它?”随后发明了UTF-8,这是一项杰出的工程。就像创建Unix时一样,Unix的创造者们思考更多,需要更长时间,资金上也较少成功,但最终做对了。
我无法评论Perl(但我认为Perl社区中的反Windows情绪比Python社区更强),但关于Python,我知道BDFL(不喜欢Windows)已经表示,在所有平台上提供足够的Unicode支持是一个重要目标。

2
+1 对于一个经常让我感到沮丧的问题,这是一个非常有启发性的观点。 - David Heffernan
我已经接受了这个答案,因为它是唯一一个真正尝试从字面上回答我的问题的答案,尽管我仍然没有办法在Perl或Python中将Unicode输出到Windows控制台!但我有一些进一步的评论: - hippietrail
1
wprintf()和相关函数是标准C库的一部分还是纯粹的MS扩展?iconv()是标准C库的一部分吗?Perl或Python是否在某个地方声明它们严格遵守标准C库并避免可能是扩展的东西,例如wprintf()和iconv()?顺便说一句,我以前在C/C++中做过Unicode,用于跨平台的AbiWord文字处理器中实现了编码文本的保存和加载功能。但是现在我更喜欢脚本语言,因为我主要处理多语言文本。 - hippietrail
2
@hippietrail:wprintf是标准C,但_setmode_fileno不是。通常(但并非总是),微软会在非标准扩展前加下划线。iconv不是C标准的一部分。Perl和Python都没有使用纯C而没有扩展,因为即使一些非常常见的事情,如读取目录内容或创建链接也不包括在C标准中。Lua在其标准库中仅使用标准C函数,但即使如此,它也必须使用扩展来进行动态模块加载。 - Philipp
2
如果Perl和Python不使用Microsoft的Unicode扩展输出,那么你就必须自己处理。在Windows中,控制台输出总是需要通过WriteConsoleW进行,没有其他方法。例如,参见这个长讨论(其中许多贡献者错误地认为Unicode在Windows控制台中无法工作或与代码页有关)。它包含了一个可能的修复链接,但通常情况下,Python标准库必须被重写。 - Philipp

9

对讨论做出小贡献 - 我正在运行捷克本地化的Windows XP,几乎在所有地方都使用CP1250代码页。然而,有趣的是控制台仍然使用传统的DOS 852代码页。

我能够编写非常简单的Perl脚本,使用以下方法将utf8编码数据打印到控制台:

binmode STDOUT, ":utf8:encoding(cp852)";

尝试了各种选项(包括utf16le),但只有以上设置才能正确打印带重音符号的捷克字符。
编辑:我进一步研究了这个问题,并找到了Win32::Unicode。该模块导出了函数printW,在输出和重定向方面都可以正常工作。
use utf8;
use Win32::Unicode;

binmode STDOUT, ":utf8";
printW "Příliš žluťoučký kůň úpěl ďábelské ódy";

2
旧的IBM代码页(如852)用于兼容性,因为它们包括许多旧DOS应用程序中使用的图形字符 - 而且其中许多仍在使用!新的代码页(如1250)是为Windows引入的,不包括控制台应用程序所需的遗留图形字符。 - hippietrail
@hippietrail 我意识到有在保持向后兼容性方面的合理性。同时感谢你提到了 chcp,我之前不知道它的存在。那么有没有办法使用它来启用 utf-8 呢?虽然让 Perl 输出 utf-8 很容易,但好像让控制台显示它就比较困难。 - bvr
1
@bvr:“chcp 65001”启用UTF-8,但似乎支持不太好。它会导致Perl输出异常损坏,并导致Python崩溃! - hippietrail
2
@bvr:是的,我也遇到了同样的问题。我不确定它是否完全是Windows的错,还是Windows和Perl之间的某种交互作用,但我认为是前者。我相当确定这是由于字符串函数假定字节数等于字符数所致。 - hippietrail
1
@hippietrail 我找到了一个能正常工作的方法 - 使用 Win32::Unicode 模块。我在我的回答中添加了一个例子。 - bvr
显示剩余6条评论

7

我需要取消你的许多问题。

你知道吗:

  • Windows在其API中使用UTF-16,但仍然默认为各种“有趣”的遗留编码(例如Windows-1252、Windows-1251)在用户空间中,包括文件名,在Windows的许多本地化版本中不同?
  • 你需要对输出进行编码,并通过locale pragma选择适当的系统编码,而这是建立在locale POSIX标准之上的,而Windows与之不兼容?
  • Perl已经支持所谓的“宽”API了吗?
  • Microsoft设法将UTF-8调整到其字符编码的代码页系统中,您可以通过发出适当的chcp 65001命令切换终端吗?

1
传统的API函数仍然可用,但它们除了将字符串转换为UTF-16并调用UTF-16函数外,没有其他作用。现在任何明智的Windows应用程序都直接使用UTF-16函数。 - Philipp
1
我知道Windows在其API中使用UTF-16,但您对于传统编码是错误的。它们根本不是默认设置,只是为了支持传统内容而存在。除了传统文件系统之外,所有内容在内部都是UTF-16,包括文件名。 - hippietrail
@hippietrail:我的评论是对短语“但仍然在用户空间默认使用各种‘有趣’的遗留编码(例如Windows-1252,Windows-1251)”的补充,我认为这不完全正确,因为这些旧函数并不比UTF-16函数更加默认。 - Philipp
2
我不知道为什么这么多错误的信息会得到6个赞! - David Heffernan
2
你知道Windows已正式符合POSIX标准吗?你知道在Windows 7中,代码页65001在控制台中仍然存在严重问题吗?Perl可以在其中工作,但字符长度与字节长度之间似乎存在一个错误,导致额外的空白行和长行的末尾被输出第二次。而Python则直接崩溃。如果它能够正常工作,我会认为这是一种有用的解决方案,但并不是从所谓的跨平台脚本语言中输出Unicode的真正解决方案。 - hippietrail
要么代码页65001与Unicode不完全相同,要么在Windows 7的中文版本上,它的默认控制台字体有一些错误字符。 - Jeremy List

5

一系列的情况交织在一起,留下了一个石头没有翻转... - Jeff
@Jeff 更新了帖子。谢谢。 - Sinan Ünür

4

你确定你的脚本在其他平台上能正确输出Unicode吗?"wide character in print"警告让我非常怀疑。

我建议查看这个概述


2
这其实是一个有效的响应。如果您从Perl收到“在打印中的宽字符”警告,则意味着您的代码有误,且在所有系统上都无法正常工作。 - hobbs
1
如果我知道我正在打印到UTF-8控制台,就像在*nix上可能的那样,我可以执行“binmode STDOUT,':utf8'”,但是在Windows上,即使代码“binmode STDOUT,':utf16'”不会引发任何错误,它也无法正常工作。因此,在跨平台代码中,除非您有实际的修复建议,否则事情将处于非常难以忍受的位置。 - hippietrail

3
多年来,为什么他们不直接调用输出UTF-16 Unicode的Win32-W API,而是强制将所有内容通过ANSI/codepage瓶颈处理呢?因为Perl和Python不是Windows程序。它们是Unix程序,巧合的是被大部分移植到了Windows上。因此,除非必要,它们不喜欢调用Win32函数。对于基于字节的I/O,这并不是必要的;可以使用标准C库完成。基于UTF-16的I/O是一种特殊情况。
-W API本质上有缺陷吗,以至于不能直接使用吗?我不会说-W API本质上有缺陷,而更倾向于认为微软在C(++)中处理Unicode的方法本质上有缺陷。无论某些Windows开发人员多么坚持认为程序应该使用wchar_t而不是char,切换存在太多障碍:
平台依赖性: 在Windows上使用UTF-16 wchar_t,在其他地方使用UTF-32 wchar_t。(新的char16_t和char32_t类型可能有所帮助。) UTF-16文件名函数(如_wfopen、_wstat等)的非标准化限制了在跨平台代码中使用wchar_t的能力。 教育。每个人都学习使用printf("Hello, world!\n");,而不是wprintf(L"Hello, world!\n");。我在大学使用的C语言教材甚至在附录A.13中都没有提到宽字符。 现有的数百万行使用char*字符串的代码。

显然,Perl和Python是*nix的移植版本,但在Python自己的网站www.python.org上,他们并没有贬低Windows的支持,事实上,他们将其列为首选!“Python可以运行在Windows、Linux/Unix、Mac OS X上,并已被移植到Java和.NET虚拟机中。”(Perl的网站则不太突出)。也许他们应该更加谦虚地承认Windows是二等公民,或者在文本在操作系统和解释器之间移动的边缘处调用iconv() / WideCharToMultiByte() / MultiByteToWideChar()。 - hippietrail
1
我必须承认,我一直以为 _wfopen 的意思更多是某种不太雅观的东西。☺ - tchrist

2
为了使Perl完全支持Windows,需要修改每个调用print printf say warndie的地方。
  • 这是Windows吗?
  • 哪个版本的Windows? Perl仍然大多数情况下可以在Windows 95上工作
  • 这是要输出到控制台还是其他地方。

确定了这些问题之后,您就必须使用完全不同的API函数集。

如果您真的想看到正确执行此操作所涉及的所有内容,请查看source中的Win32::Unicode::Console


在Linux、OpenBSD、FreeBSD和类似的操作系统上,通常可以在STDOUTSTDERR文件句柄上调用binmode
binmode STDOUT, ':utf8';
binmode STDERR, ':utf8';

这假设终端正在使用UTF-8编码。

就像有些人理论上可以在Windows 95上运行Perl而没有完全的宽功能支持一样,一些人理论上可能正在运行*nix,并将终端设置为其他编码,特别是日本用户。在这种情况下,仅调用binmode是不够的。我希望Perl可以直接调用wprintf,并且C库正确处理控制台UTF-16和重定向。如果C库出现问题,那么我当然会免除Perl的责任。 - hippietrail

2

0

Perl中的Unicode问题

涵盖了Win32控制台如何与Perl配合以及在幕后从ANSI到Unicode进行转码的过程;虽然这不仅是Perl的问题,还影响其他语言。


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