源地址连接:

http://www.tuicool.com/articles/ryuaUze

最近,我在把一个 Python 2 的视频下载工具 youku-lixian 改写成 Python 3,并添加了自己需要的 YouTube 支持。

在 Linux 下,事情进行得很顺利:所有的东西都用 UTF-8 进行编码。Python 3 里的 str 类型从 2.x 版本的 ASCII 字符串变成了 Unicode 字符串;我移除了原来代码里关于本地编码类型的判断处理部分。程序从抓取的页面上解析出视频标题部分的 Unicode 字符串,直接 print ()显示到标准输出,一切看起来很和谐。

假定我抓取的这个视频标题是中文,叫做“你好,世界”。众所周知,得益于 Python 良好的 Unicode 支持,输出它只需要简单的一句:

print ('你好,世界')

在 天杀的 Windows 7 下测试这个程序时,麻烦就出现了。如果你想知道我为什么这么说,请继续看下去。

我所不了解的 Windows

去年从学校拿到这台 Dell 笔记本时,Windows 7 自然是预装在上面的。

系统语言已经设置成了英语。很快,我对瑞典语键盘的布局感到极其不适应:它的标点符号位置与英语键盘布局有很大区别,分号、冒号、单引号双引号、斜杠反斜杠这些程序员司空见惯的符号,和美式英语键盘完全不同。于是,我把键盘布局换回了习惯的英语键盘,顺便把控制面板的“区域”选项也一概从瑞典换到了英语/美国。

在很长一段时间里,除了界面是英文以外,它看起来和以往用的中文系统没什么区别:有默认的中文字体,输入法可以添加中文的。我平时用它做的,只有:上网,挂迅雷,拿 IE 登网银,玩 Mirror's Edge,几件事而已。

文件系统是 Unicode 编码的,Web 浏览器是支持 Unicode 的,偶尔用的文本编辑器也是一律设置成 UTF-8 的。而且我们知道,从 Windows 2000 起,Windows 的内码实现是使用 UTF-16LE 的。几乎让人快要忘了还有代码页这么一回事。

可是,如果要在英文 Windows 系统的命令提示符里执行这个简单的输出 Unicode 文本的程序:

#!/usr/bin/env python
# -*- coding: utf-8 -*- if __name__ == '__main__':
print ('你好,世界')

Python 就会跳出来一段错误:

File "c:\Python32\lib\encodings\cp437.py", line 19, in encode
return codecs.charmap_encode (input,self.errors,encoding_map)[0]
UnicodeEncodeError: 'charmap' codec can't encode characters in position 0-1: character maps to <undefined>

难道 Python 3 不是支持 Unicode 的吗?难道它不是跨平台的吗?

第一个问题,基本上是对的,Python 3 确实支持 Unicode,这种支持体现在它把所有的 str 字符串都作为 Unicode 处理这件事情上。

第二个问题,不完全,跨平台的可移植性是有条件的。Python 本身是支持 Unicode,但是如果遇上了非 Unicode 的古董环境,那就一点办法也没有。

什么叫“非 Unicode 的古董环境”呢……不,我说的不是 DOS。这个东西,竟然就是 Windows 上的 cmd.exe ,每个人或多或少都用过的命令行环境。

cmd.exe ,从 MinGW 到 Python,基本上每个 Windows 下需要接触命令行的开发人员都躲不过去的东西,微软怎么就不能把它做好些?窗口大小不能随心所欲改也就算了,不能全屏显示也就算了,字体大小屏幕缓冲设置各种限制也就算了,鼠标拖拽不方便也就算了,命令行补全补不全也就算了,你好歹能把默认编码改成用 Unicode 吧?一个破窗口从二十年前的 3.x 时代沿用到今天的 Windows 7,从依赖 DOS 的command.com 到独立的 cmd.exe ,尼玛这么多年了,也没见功能上有什么实质的改进,是不是在微软眼里所有的程序员都在拿个白花花的 IDE“做你的 code”、不需要命令行了?

(在 Windows 已经完全使用 UTF-16 作为内码实现的今天, cmd.exe 仍然在使用系统默认的代码页,我所能想到的唯一理由就是为了保持和以前的 non-Unicode 程序兼容——不过这理由也太弱了吧)

微软有功夫把 Windows 8 的界面做得花里胡哨,不过看样子他们是压根不打算把cmd.exe 这个东西做得更好用些。不继续喷下去了,说处理问题的经过:

前面 Python 的错误信息里提到了个文件 cp437.py 。既然是 cp437 什么的,那就一定是 Python 在试图把 Unicode 字符串转换成用于输出的 437 代码页(英语/美国)时出了错。

为什么 Python 要把一个好端端的 Unicode 字符串转换成 cp437 呢?这很容易想通,因为程序是在 cmd.exe 这个终端环境下执行的。在我的英文系统上,它的活动代码页是 437(英语/美国)。从代码中的 Unicode 字符串到输出 cp437 的这一步转换,是由 Python 解释器来实现的,所以会由 Python 抛出一个错误,而不是直接在控制台输出一堆乱码。

首先想到的解决方案,自然是改变当前 cmd.exe 的活动代码页到 UTF-8 Unicode:

chcp.com 65001

不幸的是,这导致 Python 解释器直接崩溃了:

Fatal Python error: Py_Initialize: can't initialize sys standard streams LookupError: unknown encoding: cp65001

This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.
LookupError: unknown encoding: cp65001

搜了一下才发现,Python 3.2 目前并不支持 Windows 上面的 cp65001。话说 65001 代码页不就是 UTF-8 嘛(囧囧囧囧囧)

与其说是不支持,不如说是 bug 更合适些。因为执行之后 Windows 就跳出一个警告框说“ python.exe 已经停止响应”了……

于是,试着改变代码页到 GBK:

chcp.com 936

结果却是:

Invalid code page

Windows 声称这是一个无效的代码页。为什么?

编码是什么

好了,暂且忘记 cmd.exe 诸如此类令人不愉快的东西,在 IDLE 上试一试。

我不知道有多少 Linux 程序员写 Python 的时候会用到 IDLE。对于这些习惯了终端+文本编辑器的用户来说,IDLE 看起来是个无关紧要的附属品,也许它的定位只是用来帮助初学者入门的一个开发环境?

不过,容易被人们忽略的一点是:IDLE 本身是个跨平台的环境,这意味着它可以无条件支持 Unicode(只要系统上有相应的字体),用它来解释执行程序不必受制于特定终端环境的拘束。这一点在 Windows 上很重要,因为 cmd.exe 这玩意实在是太差劲了,所以估计很多人在 Windows 下交互执行 Python 的时候还是会选择 IDLE 的。

进入 IDLE。我们可能要关心一下这个 Windows 系统下面的默认编码方式是什么,Python 3 里面有两个函数:

>>> sys.stdout.encoding; locale.getpreferredencoding ()
'cp1252' 'cp1252'

第一个sys.stdout.encoding是指标准输出的编码,第二个locale.getpreferredencoding则是系统本地化设置的编码。两者是有区别的。现在我们看到,它们在当前环境下是相同的,都是默认的cp1252,也就是传说中的“ANSI”代码页。

恩,我们已经知道 IDLE 是一个完全跨平台的环境,所以在 IDLE 上输出 Unicode 字符可以得到和 Linux 环境下同样和谐的结果:

>>> print ('你好,世界')你好,世界

顺便看看“你好,世界”的 UTF-8 编码和 GBK 是什么,如果强制用其他编码方式来解码又会得到怎样的结果(后面也许会用到)。可以看到,5 个全角字符在 UTF-8 编码下是 15 个字节,每个字符占 3 bytes;在 GBK 编码下是 10 个字节,每个字符占 2 bytes。

虽然没有什么实际的意义,不过还是可以注意到:UTF-8 编码的字符是无法用 GBK 解码的,哪怕是乱码有时候也不行,因为可能会出现奇数字节长度,这在 GBK 下不合法;反之 GBK 编码字符亦无法用 UTF-8 解码,因为有无效字符值的存在。

借助 IDLE 看到了“你好,世界”各种编码的详细情况。现在我们可以回到 cmd.exe里面看一看下面这段程序的运行结果了:

#!/usr/bin/env python
# -*- coding: utf-8 -*- import sys, locale if __name__ == '__main__':
print(sys.stdout.encoding, locale.getpreferredencoding ()) try:
print('你好,世界')
except Exception as err:
print(str (err))

首先通过 chcp 确认, cmd.exe 的当前活动代码页是 437(英语/美国),而非 IDLE 里的 1252(ANSI)。这是由于我的 Windows 里对 non-Unicode 程序的区域设置是“英语/美国”的缘故。

程序运行的结果是:

cp437 cp1252
'charmap' codec can't encode characters in position 0-4: character maps to <undefined>

可以看到 sys.stdout.encoding 实际上就是当前环境下活动代码页的值。locale.getpreferredencoding () 没变,仍然是系统默认的 cp1252。

之后抛出的异常是在我们预料之中的,正如此前一样,Python 尝试把 Unicode 字符串转换成 cmd 终端下的 cp437 代码页编码。而中文字符本来就是没有对应的 cp437 编码的,所以 Python 报错。

Google 一下 'charmap' codec can't encode characters in position 0-4: character maps to <undefined> 这个错误。在 Stack Overflow 上,有人提到了解决的方法:设置一个叫做 PYTHONIOENCODING 的环境变量。

PYTHONIOENCODING 环境变量

所谓的 PYTHONIOENCODING ,既可以作为环境变量存在,也可以作为 Python 的命令行参数传递。它用于指定 Python 程序标准输入输出(stdin/stdout/stderr)的编码。(注意这个编码不是指源代码的编码,和 Python 程序开头常见的 # -*- coding: utf-8 -*- 是两码事)

在没有这个环境变量时

如前面所述,Python 会试图把内部 Unicode 编码的字符串转化成当前执行程序的终端环境下所使用的编码方式( sys.stdout.encoding )后输出。对于当前代码页 437 的 cmd.exe 来说,把只含有英文数字的字符串转成 cp437 编码没有任何问题;但是一旦遇上了中文字符,英语/美国的 437 代码页里必然是找不到对应的编码的,于是 Python 就会报错。

如果当前代码页设成 65001,Python 3.2 会崩溃,这是本身实现上的 问题 。在最新的 Python 3.3 beta 中已经 增加了对 cp65001 的支持 。

在设置了这个环境变量时

通过

set PYTHONIOENCODING=utf-8

或(PowerShell 下)

$env:PYTHONIOENCODING = "utf-8"

PYTHONIOENCODING 指定的编码方式会覆盖原来的 sys.stdout.encoding 。如果将 PYTHONIOENCODING 设置为 utf-8,那么 Python 在输出 Unicode 字符串的时候就会以 UTF-8 输出,相当于什么也不转换。

再次执行该 Python 程序,这一次 Python 不再尝试自动转换 Unicode 的中文字符到 cp437 中的对应字符,程序成功运行, sys.stdout.encoding 变成了 utf-8,字符串输出则是乱码:

utf-8 cp1252你好,世界

这与我们之前在 IDLE 中将 UTF-8 编码的文本强制用 cp437 解码得到的结果是完全相同的:

>>> print(bytes ('你好,世界', 'utf-8') .decode ('cp437'))
你好,世界

Python 直接把 UTF-8 编码的字符串输出到了 cp437 代码页的终端,相当于强制用 cp437 来解码 UTF-8 文本,产生了无意义的乱码。

用文本编辑器写一个内容是“你好,世界”的文件,以 UTF-8 编码保存。在 cmd.exe下通过 type 显示,结果和上面是相同的。

cmd.exe 和 PowerShell ISE 的微妙之处对比

在当前区域设置(英语/美国)下,两者执行 chcp.com 显示的当前活动代码页都是 437。

只有 cmd 下 Python 的 sys.stdout.encoding 默认是 cp437(与活动代码页相同);PowerShell ISE 下 sys.stdout.encoding 则是 cp1252(ANSI)。

locale.getpreferredencoding 永远是系统本身默认的 cp1252,这是一个系统全局值。

cmd 无法输入中文,不能正确显示文件系统中的中文文件名;PowerShell ISE 能够输入中文,能显示中文文件名。

在缺少 936 代码页的情况下,两者都不能够通过执行脚本或 type 文件内容正确显示中文字符(无论是 GBK 还是 UTF-8),会产生乱码。

为什么 Windows 会缺少 GBK 代码页?

回到最初的那个问题上来,为什么执行 chcp.com 936 不能切换到 GBK 代码页?为什么 cmd.exe 和 PowerShell 里不能正常显示中文?

这个问题让我百思不得其解。花了几个小时找到了原因,简而言之:因为 Windows 的“区域和语言”设置不对。

“Language for non-Unicode programs”这个选项不是简体中文,所以就不能用 GBK,手动 chcp.com 也会告诉你该代码页无效。所以必须要在控制面板里设置成简体中文,重启后才能生效。

好吧,问题来了,为什么这里只能单选?如果我既想使用 936(GBK)编码的应用程序,又想使用 932(日语)编码的应用程序,难道每次都要在这里改完后再重启吗?为什么他们不能给一个详细的代码页列表让用户多选、需要时可以动态加载?

Windows 设计的龌龊之处就在这里。如果你不去设置 system locale 为中文并重启,所有 non-Unicode 程序里的中文字符集都是不会出现的,只能显示成一个方框,比如cmd.exe 里:

还有 Vim 里( set fileencodings=utf-8,gbk ),GBK 编码的文本和 UTF-8 编码的文本都一样无法显示。(按理说 Vim 应该不能算 non-Unicode 程序吧……谁知道呢?!)

改过"Language for non-Unicode programs”为中文并且重启系统之后,Vim 立即显示正常:

再进 cmd.exe ,默认活动代码页 936。这段 Python 程序终于也能正确输出了:

也许 Windows 这种蛋疼的设计是因为考虑到英文用户一般不会需要多余的 Unicode 和代码页字符集,这么做可以节省系统启动时间?谁知道呢,Windows 用户不是最喜欢拿所谓的“启动时间快”作为衡量系统性能的指标了吗……

切换到 cp65001(UTF-8 Unicode), PYTHONIOENCODING 设置成 utf-8,按理来说这种方式不应该出问题,但是这输出怎么看都不像是正常(如下图所示)。不想深究到底为什么了,总之 Windows 下面东西的复杂程度以我这种智商是永远都不能够理解的……

Python 除了标准输入输出,还有……

文件名

open ('文件名测试', 'w')

Python 中对文件系统的操作基本上是不受默认编码影响的,只要sys.getfilesystemencoding () 的结果是 utf-8(现代 Linux)或者 mbcs(现代 Windows NT 系统上)。两者本质上都是 Unicode 编码。

文件输入输出

文件读写不属于标准I/O,因此和环境变量 PYTHONIOENCODING 无关。

for c in ['utf-8', 'gbk']:
with open ('test_%s.txt' % c, 'w', encoding=c) as output:
try:
output.write ('你好,世界\n')
except Exception as err:
print('\nWriting to file using %s:\n' % c, str (err))

由于在 open ()中显式指定了中文编码方式(encoding='utf-8'encoding='gbk'),输出“你好,世界”这样的中文文本在任何平台上都应该能够得到正确的结果。

然而对于:

with open ('test_default.txt', 'w') as output:
try:
output.write ('你好,世界\n')
except Exception as err:
print('\nWriting to file using default encoding:\n', str (err))

由于没有指定编码方式,Python 会自动使用系统默认的编码方式来进行输出。如果系统默认编码是 cp437 或 cp1252,由于中文字符在这些代码页中显然不存在对应值,Python 会抛出一个熟悉的错误:

File "c:\Python32\lib\encodings\cp437.py", line 19, in encode
return codecs.charmap_encode (input,self.errors,encoding_map)[0]
UnicodeEncodeError: 'charmap' codec can't encode characters in position 0-1: character maps to <undefined>

当然,当系统默认编码为 cp936(GBK)时,无论

output.write ('你好,世界')

还是

print ('你好,世界')

都可以正常工作。因为“你好,世界”这个 Unicode 字符串是可以被完全转换成 GBK 中的对应编码的。

一些总结和思考

虽然 Python 3 使用 Unicode 编码的字符串,但是在跨平台的程序中依然要取得系统的默认编码用于后续处理,因为并不是所有的终端环境都支持全部的 Unicode 字符集:

if sys.stdout.isatty ()
default_encoding = sys.stdout.encoding
else
default_encoding = locale.getpreferredencoding ()

无论何时,不要随心所欲地用 print ()向 stdout 输出 Unicode 字符串。如果某个要输出的 Unicode 字符(比如,中文字)在系统默认编码的字符集(比如,代码页 437)上没有,Python 这时就会抛出一个错误。这其实在大部分时候并不是我们想看到的局面,我们总希望即使有时会输出一些无意义的乱码,程序整体上也能正确运行。拿视频下载工具的例子来说,即使由于终端的关系有时无法正确显示视频名称,这问题并不太严重,因为程序总是可以把抓取的视频内容写入正确的文件的。

在程序中获取了系统默认的 default_encoding ,我们就可以强制用它来对 Unicode 字符串进行编码,至少避免了 Python 在自动转码过程中可能会抛出的错误——虽然结果可能只是得到一堆乱码。另外一种处理方式是对于这样的字符串,我们决定根本不去输出它们。

我们比较愿意看到的情况是:如果程序会输出且只会输出中文,而你假想中的 Windows 用户群所使用的代码页是 936(GBK)——尽管在程序中使用 Unicode 字符串吧,这样做不会带来任何问题。

但是,如果不能确定要处理的 Unicode 文本会限定在哪个代码页字符集的范围当中:中文?梵文?希伯来文?阿拉伯文?还是……火星文?这个时候就必须考虑到世界上还有“编码方式差异”这回事了。当然,最好的解决方式也许是:告诉用户,去他的代码页,去他的什么 437、500、936、1252……这堆诡异的数字,去他的 Bush hid the facts ,扔掉设计上如此糟糕、编码方式如此混乱和不一致的 Windows,转投一个让生活更简单的操作系统 吧。

Python--Cmd窗口运行Python时提示Fatal Python error: Py_Initialize: can't initialize sys standard streams LookupError: unknown encoding: cp65001的更多相关文章

  1. 执行Python出现LookupError: unknown encoding: cp65001解决办法

    执行Python出现LookupError: unknown encoding: cp65001错误 dos下执行以下命令即可 chcp 以上.

  2. python cmd 窗口 中文乱码 解决方法 (附:打印不同颜色)

    python cmd 窗口 中文乱码 解决方法 (附:打印不同颜色) 前言 在 python 开发中,有时候想通过cmd窗口来和用户交互,比如显示信息之类的,会比自己创建 GUI 来的方便,但是随之而 ...

  3. appium+python解决每次运行代码都提示安装Unlock以及AppiumSetting的问题

    appium+python解决每次运行代码都提示安装Unlock以及AppiumSetting的问题(部分安卓机型) 1.修改appium-android-driver\lib下的android-he ...

  4. window的cmd窗口运行git

    般情况下,我们在 Window 下安装好 git 后,在运行里面打开 cmd 窗口,在里面直接运行 git --version ,会提示“不是内部或外部命令,也不是一个可运行的程序”. 要想在cmd窗 ...

  5. 不显示cmd窗口运行jar包

    今天,打开导出的jar包,发现并不能运行,查看jar包中的META-INF文件夹下的MANIFEST.MF文件,发现MANIFEST.MF中并没有Main-Class,于是,就手动添加相应的信息,本项 ...

  6. 解决安装vc2005运行库时提示Command line option syntax error.Type Command/?for Help

    安装vc2005运行库时提示 这是因为它要自解压到用户的临时文件夹下,如果用户名中带中文,就会报错. 简单的解决方法是,手动解压之,再安装 当然,你也可以修改用户名或者再新建个用户.

  7. LoadRunner中运行场景时提示"You do not have a license for this Vuser type."

    LoadRunner中运行场景时提示"You do not have a license for this Vuser type." 2012-06-15 17:09:07|  分 ...

  8. centos 在安装YouCompleteMe时提示 Fatal : pyconfig.h No such file or directory

    问题:centos 在安装YouCompleteMe时提示 Fatal : pyconfig.h No such file or directory 解决:安装python-devel yum ins ...

  9. 解决Docker运行命令时提示"Got permission denied while trying to connect to the Docker daemon socket"类情况

    Docker安装命令: 解决Docker运行命令时提示"Got permission denied while trying to connect to the Docker daemon ...

随机推荐

  1. 开发笔记:用不用UnitOfWork以及Repository返回什么集合类型

    这2天实际开发中明确的东西,在这篇博文中记录一下. 之前对是否需要自己封装UnitOfWork有些犹豫,因为Entity Framework就是一个UnitOfWork实现, 自己再封装一下显得有些多 ...

  2. Unity2.0容器自动注册机制

    现如今可能每个人都会在项目中使用着某种 IoC 容器,并且我们的意识中已经形成一些固定的使用模式,有时会很难想象如果没有 IoC 容器工作该怎么进展. IoC 容器通过某种特定设计的配置,用于在运行时 ...

  3. ConcurrentHashMap--锁的分段技术

    ConcurrentHashMap是Java 5中支持高并发.高吞吐量的线程安全HashMap实现. HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable ...

  4. Backbone Collection——数据模型集合

    如果将一个Model对象比喻成数据库中的一条记录,那么Collection就是一张数据表.它表示为一个模型集合类,用于存储和管理一系列相同类型的模型对象. 1.创建集合集合用于组织和管理多个模型,但它 ...

  5. 简单理解ECMAScript2015中的箭头函数新特性

    箭头函数(Arrow functions),是ECMAScript2015中新加的特性,它的产生,主要有以下两个原因:一是使得函数表达式(匿名函数)有更简洁的语法,二是它拥有词法作用域的this值,也 ...

  6. atitit.组件化事件化的编程模型--服务端控件(1)---------服务端控件与标签的关系

    atitit.组件化事件化的编程模型--服务端控件(1)---------服务端控件与标签的关系 1. 服务器控件是可被服务器理解的标签.有三种类型的服务器控件: 1 1.1. HTML 服务器控件  ...

  7. atitit.基于http json api 接口设计 最佳实践 总结o7

    atitit.基于http  json  api 接口设计 最佳实践 总结o7 1. 需求:::服务器and android 端接口通讯 2 2. 接口开发的要点 2 2.1. 普通参数 meth,p ...

  8. paip.提升效率--gui 的选择--swing最佳实践swt awt

    paip.提升效率--gui 的选择--swing最佳实践swt awt ////////////////弹出消息框. ////////////////myeclipse swing 开发最佳实践.. ...

  9. Python面试题(一)

    **晚上在公司的论坛上看到一道面试题,题目如下:随机给定一字符串和字符,要求重排,比如:'abde','c'.重排之后变成'abcde' **看到他们给的答案很多都是二分法重排,既然是字符类的处理,当 ...

  10. python中os和sys模块的详解

    平时在工作中经常会用到os模块和sys模块的一些特性,下面是这些特性的一些相关解释,希望对大家有所帮助 os模块 os.getcwd() 获取当前工作目录,即当前python脚本工作的目录路径 os. ...