Python 编码为什么那么蛋疼?

2017-03-20 14:15:29 +08:00
 lzjun

据说,每个做 Python 开发的都被字符编码的问题搞晕过,最常见的错误就是 UnicodeEncodeError 、 UnicodeDecodeError ,你好像知道怎么解决,遗憾的是,错误又出现在其它地方,问题总是重蹈覆辙, str 到 unicode 之间的转换用 decode 还是 encode 方法还特不好记,老是混淆,问题究竟出在哪里?

为了弄清楚这个问题,我决定从 python 字符串的构成以及字符编码的细节上进行深入浅出的分析

字节与字符

计算机存储的一切数据,文本字符、图片、视频、音频、软件都是由一串 01 的字节序列构成的,一个字节等于 8 个比特位。

而字符就是一个符号,比如一个汉字、一个英文字母、一个数字、一个标点都可以称为一个字符。

字节方便存储和网络传输,而字符用于显示,方便阅读。例如字符 "p" 存储到硬盘是一串二进制数据 01110000,占用一个字节的长度

编码与解码

我们用编辑器打开的文本,看到的一个个字符,最终保存在磁盘的时候都是以二进制字节序列形式存起来的。那么从字符到字节的转换过程就叫做编码( encode ),反过来叫做解码( decode ),两者是一个可逆的过程。编码是为了存储传输,解码是为了方便显示阅读。

例如字符 "p" 经过编码处理保存到硬盘是一串二进制字节序列 01110000 ,占用一个字节的长度。字符 "禅" 有可能是以 "11100111 10100110 10000101" 占用 3 个字节的长度存储,为什么说是有可能呢?这个放到后面再说。

Python 的编码为什么那么蛋疼?当然,这不能怪开发者。

这是因为 Python2 使用 ASCII 字符编码作为默认编码方式,而 ASCII 不能处理中文,那么为什么不用 UTf-8 呢?因为 Guido 老爹为 Python 编写第一行代码是在 1989 年的冬天, 1991 年 2 月正式开源发布了第一个版本,而 Unicode 是 1991 年 10 月发布的,也就是说 Python 这门语言创立的时候 UTF-8 还没诞生,这是其一。

Python 把字符串的类型还搞成两种, unicode 和 str ,以至于把开发者都弄糊涂了,这是其二。 python3 彻底把 字符串重新改造了,只保留一种类型,这是后话,以后再说。

str 与 unicode

Python2 把字符串分为 unicode 和 str 两种类型。本质上 str 是一串二进制字节序列,下面的示例代码可以看出 str 类型的 "禅" 打印出来是十六进制的 \xec\xf8 ,对应的二进制字节序列就是 '11101100 11111000'。

>>> s = '禅'
>>> s
'\xec\xf8'
>>> type(s)
<type 'str'>

而 unicode 类型的 u"禅" 对应的 unicode 符号是 u'\u7985'

>>> u = u"禅"
>>> u
u'\u7985'
>>> type(u)
<type 'unicode'>

我们要把 unicode 符号保存到文件或者传输到网络就需要经过编码处理转换成 str 类型,于是 python 提供了 encode 方法,从 unicode 转换到 str ,反之亦然。

encode

>>> u = u"禅"
>>> u
u'\u7985'
>>> u.encode("utf-8")
'\xe7\xa6\x85'

decode

>>> s = "禅"
>>> s.decode("utf-8")
u'\u7985'
>>>

不少初学者怎么也记不住 str 与 unicode 之间的转换用 encode 还是 decode ,如果你记住了 str 本质上其实是一串二进制数据,而 unicode 是字符(符号),编码( encode )就是把字符(符号)转换为 二进制数据的过程,因此 unicode 到 str 的转换要用 encode 方法,反过来就是用 decode 方法。

encoding always takes a Unicode string and returns a bytes sequence, and decoding always takes a bytes sequence and returns a Unicode string".

清楚了 str 与 unicode 之间的转换关系之后,我们来看看什么时候会出现 UnicodeEncodeError 、 UnicodeDecodeError 错误。

UnicodeEncodeError

UnicodeEncodeError 发生在 unicode 字符串转换成 str 字节序列的时候,来看一个例子,把一串 unicode 字符串保存到文件

# -*- coding:utf-8 -*-
def main():
    name = u'Python 之禅'
    f = open("output.txt", "w")
    f.write(name)

错误日志

UnicodeEncodeError: 'ascii' codec can't encode characters in position 6-7: ordinal not in range(128)

为什么会出现 UnicodeEncodeError ?

因为调用 write 方法时, Python 会先判断字符串是什么类型,如果是 str ,就直接写入文件,不需要编码,因为 str 类型的字符串本身就是一串二进制的字节序列了。

如果字符串是 unicode 类型,那么它会先调用 encode 方法把 unicode 字符串转换成二进制形式的 str 类型,才保存到文件,而 encode 方法会使用 python 默认的 ascii 码来编码

相当于:

>>> u"Python 之禅".encode("ascii")

但是,我们知道 ASCII 字符集中只包含了 128 个拉丁字母,不包括中文字符,因此 出现了 'ascii' codec can't encode characters 的错误。要正确地使用 encode ,就必须指定一个包含了中文字符的字符集,比如: UTF-8 、 GBK 。

>>> u"Python 之禅".encode("utf-8")
'Python\xe4\xb9\x8b\xe7\xa6\x85'

>>> u"Python 之禅".encode("gbk")
'Python\xd6\xae\xec\xf8'

所以要把 unicode 字符串正确地写入文件,就应该预先把字符串进行 UTF-8 或 GBK 编码转换。

def main():
    name = u'Python 之禅'
    name = name.encode('utf-8')
    with open("output.txt", "w") as f:
    	f.write(name)

当然,把 unicode 字符串正确地写入文件不止一种方式,但原理是一样的,这里不再介绍,把字符串写入数据库,传输到网络都是同样的原理

UnicodeDecodeError

UnicodeDecodeError 发生在 str 类型的字节序列解码成 unicode 类型的字符串时

>>> a = u"禅"
>>> a
u'\u7985'
>>> b = a.encode("utf-8")
>>> b
'\xe7\xa6\x85'
>>> b.decode("gbk")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'gbk' codec can't decode byte 0x85 in position 2: incomplete multibyte sequence

把一个经过 UTF-8 编码后生成的字节序列 '\xe7\xa6\x85' 再用 GBK 解码转换成 unicode 字符串时,出现 UnicodeDecodeError ,因为 (对于中文字符) GBK 编码只占用两个字节,而 UTF-8 占用 3 个字节,用 GBK 转换时,还多出一个字节,因此它没法解析。避免 UnicodeDecodeError 的关键是保持 编码和解码时用的编码类型一致。

这也回答了文章开头说的字符 "禅",保存到文件中有可能占 3 个字节,有可能占 2 个字节,具体处决于 encode 的时候指定的编码格式是什么。

再举一个 UnicodeDecodeError 的例子

>>> x = u"Python"
>>> y = "之禅"
>>> x + y
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
>>>

str 与 unicode 字符串 执行 + 操作是, Python 会把 str 类型的字节序列隐式地转换成(解码)成 和 x 一样的 unicode 类型,但 Python 是使用默认的 ascii 编码来转换的,而 ASCII 中不包含中文,所以报错了。

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

正确地方式应该是显示地把 y 用 UTF-8 或者 GBK 进行解码。

>>> x = u"Python"
>>> y = "之禅"
>>> y = y.decode("utf-8")
>>> x + y
u'Python\u4e4b\u7985'

以上内容都是基于 Python2 来讲的,关于 Python3 的字符和编码将会另开一篇文章来写,保持关注。

原文地址:https://mp.weixin.qq.com/s/LQrPmp2HMlw5C7izJIUHNQ
作者:liuzhijun

7605 次点击
所在节点    Python
66 条回复
Ixizi
2017-03-20 14:58:54 +08:00
nice
hjc4869
2017-03-20 15:08:30 +08:00
看标题以为是喷 python 2 ,进来才发现是编码科普文
fy
2017-03-20 15:08:54 +08:00
为什么蛋疼? -> 请用 Python3 ,下一题。
glasslion
2017-03-20 15:13:07 +08:00
编码一直是就是一件很蛋疼的事, 其他语言看上去不那么蛋疼,无非是:
1. 不检查编码 /解码是否会报错。 手持两把锟斤拷, 口中直呼烫烫烫 就是这么来的。
2. 只支持 utf-8 这一种编码
lzjun
2017-03-20 15:22:25 +08:00
@hjc4869

Python 的编码为什么那么蛋疼?当然,这不能怪开发者。

这是因为 Python2 使用 ASCII 字符编码作为默认编码方式,而 ASCII 不能处理中文,那么为什么不用 UTf-8 呢?因为 Guido 老爹为 Python 编写第一行代码是在 1989 年的冬天, 1991 年 2 月正式开源发布了第一个版本,而 Unicode 是 1991 年 10 月发布的,也就是说 Python 这门语言创立的时候 UTF-8 还没诞生,这是其一。

Python 把字符串的类型还搞成两种, unicode 和 str ,以至于把开发者都弄糊涂了,这是其二
call43848
2017-03-20 15:28:22 +08:00
神曰:“用 3 ”。
gimp
2017-03-20 15:29:06 +08:00
@glasslion

“手持两把锟斤拷, 口中直呼烫烫烫” 笑了
Gsyc1
2017-03-20 15:57:04 +08:00
用 Python 3 ,字符串默认是 unicode 的
gouchaoer
2017-03-20 15:59:50 +08:00
我到现在也没搞懂编码,我为什么要搞清楚编码呢?我用 java 用 php 就没操过心
lzjun
2017-03-20 16:02:12 +08:00
@gouchaoer 黑的漂亮,哈哈
janxin
2017-03-20 16:08:38 +08:00
用 python3
aploium
2017-03-20 16:21:34 +08:00
from __future__ import unicode_literals
sagaxu
2017-03-20 16:22:25 +08:00
@gouchaoer
那是因为 php 只有 str 没有 unicode(php6 有,但夭折了),而 Java 只有 unicode 没有 str(str 用 byte[])。
qingshi
2017-03-20 16:28:44 +08:00
@Gsyc1 默认是 utf-8 吧
helloSwift
2017-03-20 16:29:33 +08:00
为什么你写了这么多,不去看看 Python3 呢(´・_・`)
keisuu
2017-03-20 16:57:01 +08:00
@helloSwift 楼主分析的是 python2 。

看完全文,我算是了解为什么老是报编码错误的原因了。

ps: python3 一样的有类似的错误吧。
lzjun
2017-03-20 17:09:52 +08:00
@helloSwift 啥意思? Python3 好呀
everhythm
2017-03-20 17:47:07 +08:00
python 3 还是有编码错呀

比如你用 vim 打开 1 个文件,生成个 .swp 文件

如果用 python 读取到这个文件,就报错
hjc4869
2017-03-20 17:50:11 +08:00
@lzjun 其实用 byte[]来表示字符串也算是 UNIX 和 C 的遗毒了。现代语言在设计的时候基本都是以 code point 为单位,虽然 Java 等语言被 UCS-2 坑了…
thekll
2017-03-20 18:36:59 +08:00
很奇怪的 coding 方式。
是说内存中保存 unicode code point , I/O 时再编码 /解码吗?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/348820

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX