Python 编码错误的本质和解决方案

Python 常见编码错误 UnicodeDecodeError 和 UnicodeEncodeError 的原理和解决方案

基础概念

字符

character 构成文本的最小组成单元

字节

byte 数据在计算机内部的存储单元,一个字节等于一个8位的比特,计算机中的所有数据都是由字节组成

字符集

Character set 由多个字符的组成的集合,常见的字符集有ASCII、Unicode、GB2312等

字符编码值

不同的字符集规定了不同的编码规则,编码规则中规定了字符对应的编码值 code point,一个整数值

编码

将字符集中的字符码根据 code point 映射为字节流(byte sequence)的一种具体实现

解码

将字节流解析为字符集中的字符


文中 python 皆为 2.x 版本code

初学 python 的人基本上都有过如下类似经历:

UnicodeDecodeError

1
2
3
Traceback (most recent call last):
File "<input>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)

UnicodeEncodeError

1
2
3
Traceback (most recent call last):
File "<input>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

这两个错误在 python 中十分常见,一不留神就碰上了。如果你写过c、c++ 或者 java,对比之下一定会觉得 python 这个错误真让人火大。事实也确实如此,我也曾经很火大🔥。

这两个错误究竟意味着什么?可以先从 python 的基本数据类型 string 和 unicode 开始。

string

字符串(string)其实就是一段文本序列,是由一个或多个字符组成(character),字符是文本的最小构成单元,在 python 中可以用以下方式表示字符串:

1
2
3
4
5
6
7
8
9
10
11
12
>>> s1 = 'abc'
>>> s2 = "abc"
>>> s3 = """
abc
"""
>>> s4 = '中文'
>>> for i in [s1, s2, s3, s4]:
print type(i)
<type 'str'>
<type 'str'>
<type 'str'>
<type 'str'>

这些变量在 python shell 中对应输出是:

1
2
3
4
s1 --> 'abc'
s2 --> 'abc'
s3 --> '\nabc\n'
s4 --> '\xe4\xb8\xad\xe6\x96\x87'

s4 的输出和其它变量明显不同,字面上是一个 16 进制序列,但是 s4 和其它字符串一样,在 python 内部都是用同样方式进行存储的: 字节流(byte stream),即字节序列

字节是计算机内部最小的可寻址的存储单位(对大部分计算机而言),一个字节是由 8 bit 组成,也就是对应 8 个二进制位。其实可以更进一步解释说,python 不仅用字节的方式存储着变量中的字符串文本,python 文件中的所有信息在计算机内部都是用一个个字节表示的,计算机是用这样的方式存储文本数据的

字符串用字节如何表示?

答案就是编码。计算机是只能识别 0 或 1 这样的二进制信息,而不是 a 或 b 这样对人类有意义的字符,为了让机器能读懂这些字符,人类就发明字符到二进制的映射关系,然后按照这个映射规则进行相应地编码。ascii 就是这样背景下诞生的一种编码规则。ascii 也是 python 2.x 默认使用的编码规则。

ascii 规定了常用的字符到计算机是如何映射的,编码范围是 0~127 共 128 个字符。简单来说它就是一本字典,规定了不同字符的对应的编码值(code point,一个整数值),这样一来计算机就能用二进制表示了。比如字符 a 的编码是 97,对应的二进制是 1100001,一个字节就足够存储这些信息。字符串 “abc” 最终存储就是 [97] [98] [99] 三个字节。python 默认情况下就是使用这个规则对字符进行编码,对字节进行解码(反编码)。

1
2
3
4
5
>>> ord('a')
97
>>> chr(97)
'a'
>>>

由于 ascii 的编码范围非常有限,对超过 ascii 范围之外的字符,python 是如何处理的?很简单,抛错误出来,这就是 UnicodeEncodeErrorUnicodeDecodeError 的来源。那 python 会在什么时候抛出这样的错误,也就是说 python 进行编码和解码的操作发生在何时?

unicode 对象

unicode 对象和 string 一样,是 python 中的一种字符对象(python 中一切皆对象,string 也是)。先不要去想 unicode 字符集、unicode 编码或者 utf-8 这些概念,在此特意加了对象 就是为了和后面提到的 unicode 字符集进行区分。这里说的 unicode 就是 python 中的 unicode 对象,构造函数是 unicode()

在 python 中创造 unicode 对象也很简单:

1
2
3
4
>>> s1 = unicode('abc')
>>> s2 = u'abc'
>>> s3 = U'abc'
>>> s4 = u'中文'

这些变量在 python shell 中对应输出是:

1
2
3
4
s1 --> u'abc'
s2 --> u'abc'
s3 --> u'abc'
s4 --> u'\u4e2d\u6587'

同样的,s4 的输出和其它变量不同,这些就是unicode 字符。由于 ascii 能够表示的字符太少,而且不够通用(扩展 ascii 的话题,就是把 ascii 没有利用的剩下大于 127 的位置利用了,在不同的字符集里代表不同的意思),unicode 字符集 就被造出来了,一本更大的字典,里面有更多的编码值。

unicode 字符集

unicode 字符集解决了:

  • ascii 表达能力不够的问题
  • 扩展 ascii 不够通用的问题

虽然 unicode 字符集表达能力强,又能够统一字符编码规则,但是它并没有规定这些字符在计算机中是如何表示的。它和 ascii 不同,很多字符(编码值大于 255 )没有办法用一个字节就搞定。怎样做到高效快捷地存储这些编码值?于是就有了 unicode 字符集的编码规则的实现:utf-8、utf-16等。

到这里可以简单理清 ascii、unicode 字符集、utf-8等的关系了:ascii 和 unicode 字符集都是一种编码集,由字符和字符对应的整数值(code point)组成,ascii 在计算机内部用一个字节存储,utf-8 是 unicode 字符集存储的具体实现,因为 unicode 字符集没有办法简简单单用一个字节搞定。

回到 s4 对应的输出,这个输出就是 unicode 字符集对应的编码值(code point)的 16 进制表示

unicode 对象是用来表示 unicode 字符集中的字符,这些字符(实际是那个编码值,一个整数) 在 python 中又是如何存储的?有了前文的分析,也许可以猜到,python 依然是通过编码然后用字节的方式存储,但是这里的编码就不能是 ascii 了,而是对应 unicode 字符集的编码规则: utf-8、utf-16等。

unicode 对象的编码

unicode 对象想要正确的存储就必须指定相应的编码规则,这里我们只讨论使用最广泛的 utf-8 实现。

在 python 中对 unicode 对象编码如下:

1
2
3
4
5
>>> s=u'中文'
>>> s.encode('utf-8')
'\xe4\xb8\xad\xe6\x96\x87'
>>> type(s.encode('utf-8'))
<type 'str'>

编码之后输出的是个 string 并以字节序列的方式进行存储。有了编码就会有解码,python 正是在这种编码、解码的过程使用了错误的编码规则而发生了 UnicodeEncodeErrorUnicodeDecodeError 错误,因为它默认使用 ascii 来完成转换

string 和 unicode 对象的转换

unicode 对象可以用 utf-8 编码为 string,同理,string 也可以用 utf-8 解码为 unicode 对象

1
2
3
4
5
6
7
8
9
10
>>> u=u'中文'
>>> s = u.encode('utf-8')
>>> s
'\xe4\xb8\xad\xe6\x96\x87'
>>> type(s)
<type 'str'>
>>> s.decode('utf-8')
u'\u4e2d\u6587'
>>> type(s.decode('utf-8'))
<type 'unicode'>

错误的编码规则就会导致那两个常见的异常

1
2
3
4
5
6
7
8
9
>>> u.encode('ascii')
Traceback (most recent call last):
File "<input>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
>>>
>>> s.decode('ascii')
Traceback (most recent call last):
File "<input>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)

这两个错误在某些时候会突然莫名其妙地出现就是因为 python 自动地使用了 ascii 编码。

python 自动解编码

1.stirng 和 unicode 对象合并

1
2
3
4
5
>>> s + u''
Traceback (most recent call last):
File "<input>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
>>>

2.列表合并

1
2
3
4
5
>>> as_list = [u, s]
>>> ''.join(as_list)
Traceback (most recent call last):
File "<input>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)

3.格式化字符串

1
2
3
4
5
>>> '%s-%s'%(s,u)
Traceback (most recent call last):
File "<input>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
>>>

4.打印 unicode 对象

1
2
3
4
5
6
7
8
9
10
#test.py
# -*- coding: utf-8 -*-
u = u'中文'
print u

#outpt
Traceback (most recent call last):
File "/Users/zhyq0826/workspace/zhyq0826/blog-code/p20161030_python_encoding/uni.py", line 3, in <module>
print u
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

5.输出到文件

1
2
3
4
5
6
>>> f = open('text.txt','w')
>>> f.write(u)
Traceback (most recent call last):
File "<input>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
>>>

1,2,3 的例子中,python 自动用 ascii 把 string 解码为 unicode 对象然后再进行相应操作,所以都是 decode 错误, 4 和 5 python 自动用 ascii 把 unicode 对象编码为字符串然后输出,所以都是 encode 错误。

只要涉及到 unicode 对象和 string 的转换以及 unicode 对象输出、输入的地方可能都会触发 python 自动进行解码/编码,比如写入数据库、写入到文件、读取 socket 等等。

到此,这两个异常产生的真正原因了基本已经清楚了: unicode 对象需要编码为相应的 string(字符串)才可以存储、传输、打印,字符串需要解码为对应的 unicode 对象才能完成 unicode 对象的各种操作,lenfind 等。

1
2
string.decode('utf-8') --> unicode
unicode.encode('utf-8') --> string

如何避免这些的错误

1.理解编码或解码的转换方向

无论何时发生编码错误,首先要理解编码方向,然后再针对性解决。

2.设置默认编码为 utf-8

在文件头写入

1
# -*- coding: utf-8 -*-

python 会查找: coding: name or coding=name,并设置文件编码格式为 name,此方式是告诉 python 默认编码不再是 ascii ,而是要使用声明的编码格式。

3.输入对象尽早解码为 unicode,输出对象尽早编码为字节流

无论何时有字节流输入,都需要尽早解码为 unicode 对象。任何时候想要把 unicode 对象写入到文件、数据库、socket 等外界程序,都需要进行编码。

4.使用 codecs 模块来处理输入输出 unicode 对象

codecs 模块可以自动的完成解编码的工作。

1
2
3
4
>>> import codecs
>>> f = codecs.open('text.txt', 'w', 'utf-8')
>>> f.write(u)
>>> f.close()

参考文献

注意:转载请注明出处和文章链接

三月沙 wechat
扫描关注 wecatch 的公众号