最近在利用 Python 获取网络数据的过程中出现了一个问题,系统会时不时抛出 UnicodeEncodeError 错误并停止运行,严重影响了其它功能的运行。程序类似下面这样:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import urllib2
print urllib2.urlopen(" http://www.example.com/examplepath").read()
报错信息如下:
UnicodeEncodeError: 'gbk' codec can't encode character
u'\u2022' in position XXXX: illegal multibyte sequence
经过检查,发现是因为获取的远程文件中含有“·”这个字符,官方解释是“着重号(BULLET)”,其 Unicode 编码是 U+2022。也就是说,因为获取到的远程内容中含有这个字符,而当前输出环境使用的 GBK 编码无法表示它,所以程序出现了 UnicodeEncodeError。
在 Python Wiki 页面中,找到了一篇关于 PrintFails 的说明:
如果用 print 函数向控制台输出一个 Unicode 字符串,而当前控制台编码无法表示这个字符,就会出现类似错误。
这段说明的核心意思是:Python 控制台输出时,会经过 sys.stdout 以及相应的文本输出层,最终根据目标编码输出到控制台。如果目标编码无法表示当前字符,就会发生 UnicodeEncodeError。
要理解这个问题,我们先分析一下文件编码和变量编码。
文件编码与变量编码
Python 内部是如何处理各种不同编码的呢?当程序文件的编码和字符串使用的编码不一样时,Python 会进行怎样的处理?
世界上存在各种各样的字符编码,比如 GB2312、GBK、ASCII,以及我们现在经常使用的 UTF-8。很多编码都兼容 ASCII,这也是不同编码可以共同使用英文字符的原因。
那么 Python 如何对不同编码文件中的字符串进行处理呢?例如:
#!/usr/bin/env python
# -*- coding:gbk -*-
stra = "你"
strb = u"你"
print stra
print strb
print stra.encode("hex")
print hex(ord(strb))
print strb.encode("utf-8").encode("hex")
print isinstance(stra, str)
print isinstance(strb, unicode)
这个 Python 文件以 GBK 的方式保存,并在文件头部声明了编码方式为 GBK。
第三行和第四行分别声明了两个字符串变量,不同的是 strb 使用了 Unicode 字符串。
这里需要注意,Python 2 中的 str 和 unicode 是两种不同的字符串类型。str 本质上保存的是字节数据,而 unicode 保存的是 Unicode 字符。
把上面的程序文件编码改成 UTF-8 或其它编码,可以看到 str 字符串字节内容会随源文件编码发生变化,而 Unicode 字符串本身的 Unicode 编码值并不会因此改变。
简单来说:
- Python 2 中,普通
str字符串保存的是字节数据; unicode字符串保存的是 Unicode 字符;- 源代码文件的编码会影响字符串字面量产生的字节内容;
- Unicode 字符串本身不依赖源文件编码保存字符。
编码转换
Python 提供 encode 和 decode 两个方法处理字符串编码。
encode 是将 Unicode 字符编码成指定的字节编码;decode 则是将指定编码的字节数据解码成 Unicode。
例如:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
stra = "Hello"
print stra.encode("gbk").encode("hex")
print stra.decode("utf-8").encode("hex")
这里可以简单理解为:
字节数据 --decode--> Unicode
Unicode --encode--> 指定编码的字节数据
所以,在不同编码之间转换时,一般应该先 decode 成 Unicode,再 encode 成目标编码。
Python代码转换原理
Unicode 字符使用一个统一的代码点(code point)表示,范围从 0 到 0x10FFFF。
当 Unicode 字符需要转换成其它编码时,就需要查找对应的编码规则。
以 ASCII 为例:
- 如果代码点小于 128,可以直接表示;
- 如果代码点大于等于 128,则 ASCII 无法表示这个字符,Python 就可能抛出
UnicodeEncodeError。
UTF-8 则可以表示所有 Unicode 代码点:
- ASCII 范围的字符使用单字节;
- 更大的代码点使用多字节表示;
- Unicode 中的字符都可以映射到 UTF-8。
UTF-8 的一个重要优势就是兼容 ASCII,并且能够表示 Unicode 中的所有字符。
而对于中文来说,Unicode 转 GBK 则需要根据具体的映射表进行转换。如果目标编码中不存在对应字符,同样会出现编码错误。
Python字符输出流程
前面讲到了 Python 字符串的编码和转换方式。当编码转换过程中遇到目标编码无法表示的字符时,就会抛出 UnicodeEncodeError。
那么 Python 是如何输出一个字符的呢?为什么同一个字符在 Windows 下输出可能报错,而在 Linux 下却可以正常输出?
Python IO
Python 提供了多层 IO 结构,用来处理不同层次的数据。其中可以简单理解为:
- Raw IO:处理原始字节
- Buffered IO:提供缓冲
- Text IO:处理字符以及编码和解码
在 Python 的输出过程中,sys.stdout 会负责将程序输出的数据传递给终端。对于文本输出层来说,它需要按照当前输出环境的编码方式,将 Unicode 字符转换成对应的字节数据。
例如 Windows 中文控制台通常使用 cp936(GBK),如果输出的 Unicode 字符在 GBK 中不存在对应编码,就会出现:
UnicodeEncodeError
而 UTF-8 可以表示所有 Unicode 字符,因此使用 UTF-8 输出时通常不会遇到这类问题。
Unicode Output
所有 Unicode 对象都有 encode 方法。当 Unicode 对象需要输出时,最终都需要转换成目标编码的字节数据。
所以,一个 Unicode 字符从程序内部到最终显示出来,实际上可能经历多次处理:
Unicode对象
↓
encode
↓
输出层
↓
目标编码
↓
终端 / 文件
如果目标编码无法表示当前 Unicode 字符,就会在 encode 的过程中抛出 UnicodeEncodeError。
这也就解释了最开始的问题:程序获取到的内容本身没有问题,真正的问题出现在输出阶段。远程内容中包含 U+2022 这样的 Unicode 字符,而 Windows 控制台使用 GBK 编码,GBK 无法表示这个字符,于是 Python 在输出时产生了 UnicodeEncodeError。
所以,处理 Unicode 字符串时,一个比较重要的原则就是:
内部尽量统一使用 Unicode,最终输出或保存时,再根据目标环境选择合适的编码。
评论0
欢迎分享你的看法,也欢迎补充不同的实践经验。