Python 2 字符串编码踩坑小结

Tips: 如果您已经充分理解问题是什么,请直接跳到 #问题出在哪里 一节。

字符串和编码

先从概念说起,字符串和它的编码是两个不同的概念:

  • 字符串是一段文字本身,可以是中文可以是英文,以及各种语言
  • 字符串的编码是计算机存储、处理字符串的方式;作为一种数据,它和其他数据一样,都是以一串0和1组成的,通常我们用字节数组来表示它。

字符串经过编码(encode) 就成为了一堆数据,反过来,数据经过解码(decode) 就变回我们认识的字符串。

从 Python 3 说起

这个编码问题(坑)可以说是 Python 2 被吐槽最多的黑点,没有之一。为了防止上来就掉进 Python 2 的坑里,我们先来看看 Python 3 里“改进”后是什么样子的。

1
2
3
4
5
>>> s = "Hello, 世界"
>>> type(s)
<class 'str'>
>>> len(s)
9

哈,没有任何问题!(数长度的时候别漏了空格)

查阅文档,我们发现 str 有个函数叫 encode(),它看起来很眼熟,让我们来试试:

1
2
3
>>> b = s.encode("utf-8")
>>> b
b'Hello, \xe4\xb8\x96\xe7\x95\x8c'

这个 b'' 的前缀表示返回值是一个 bytes 变量,也就是一堆数据了。

为什么这里面"Hello"还是原来的样子,但是“世界”变成一坨 \x?? 了?

这是因为 ASCII 实在太有名了,程序员们都看得懂:这个 H 其实表示的是一字节 0x48。而后面“世界”的编码不在 ASCII 的编码范围内,所以只能用 \x?? 表示了。

这样看起来也许更清晰一些:

1
2
>>> b.hex()
'48656c6c6f2c20e4b896e7958c'

encode() 当然也有 decode()。我们对刚刚拿到的 bytes b 解码,果然会变成原来的字符串。

1
2
>>> b.decode('utf-8')
'Hello, 世界'

OK,现在你已经明白了奥义所在,是时候去踩坑了。

Python 2 的世界

初见茅庐

先来一道开胃菜:

1
2
3
4
5
>>> s = "Hello, 世界"
>>> type(s)
<type 'str'>
>>> len(s)
13

▲ 为什么这个长度是 13 ?明明是 9 个字符啊!

1
2
>>> s
'Hello, \xe4\xb8\x96\xe7\x95\x8c'

s 你怎么坏掉了?

1
2
3
>>> b = s.encode('utf-8')
>>> b
'Hello, \xe4\xb8\x96\xe7\x95\x8c'

▲ 我可能用了假的 encode()

1
2
>>> b.decode('utf-8')
u'Hello, \u4e16\u754c'

▲ 喵喵喵?

以上,Python 2 中字符串并不像我们想的那样工作。

问题出在哪里

其实说起来也简单,Python 是一门诞生于 1989 年的古老语言,比 Unicode 还要早两年,当时的程序员并不在乎编码问题,因为 ASCII 已经足够了。

如果你熟悉 C/C++ 会发现同样的问题:char* 被同时用于表示字符串和字节数组。Python 2 里也是同样,str 其实是个字节数组,却被挂上了字符串的名字。二十年后用着中文字符的我们被坑惨了。

后来 Python 2 为了支持 Unicode,增加了 unicode 类型,然而并没有卵用——程序员们不记得在每个字符串前面加上 u,这也不够优雅。

Python 3 设计之初就立志解决这个问题,不惜彻底修改了str的定义,把 str 这个名字让给了原来的 unicode,而新增的 bytes 类型才是字节数组。如下表所示:

Python 2 Python 3
字符串(Unicode) unicode str
字节数组 str (bytes) bytes

所以,在 Python 2 里,如果遇到非英语字符,一定要记得用 unicode。效果是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
>>> s = u'Hello, 世界'
>>> s
u'Hello, \u4e16\u754c'
>>> type(s)
<type 'unicode'>
>>> len(s)
9
>>> b = s.encode('utf-8')
>>> b
'Hello, \xe4\xb8\x96\xe7\x95\x8c'
>>> b.decode('utf-8')
u'Hello, \u4e16\u754c'

至于为什么 str 也有 encode(),主要是为了尽可能保持和 Python 3 的兼容性,以让部分程序能在 2、3 同时运行。于是事情变得更糟糕了。

原来如此

现在我们可以解释刚刚遇到的奇怪情况了:

  • “为什么这个长度是 13 ?明明是 9 个字符啊!”——因为 Python 自动帮你编码了,编码后是 13 个字节,常见的汉字在 UTF-8 编码下为 3 个字节
  • s 你怎么坏掉了?” ——str 本来就是字节数组
  • “我可能用了假的 encode()”——你不应该对 str 变量做 encode,它本来就是编码后的
  • “喵喵喵?”——这是正常的,只是因为 Python 2 没有把 Unicode 字符显示成中文字符,用 print 就没问题了:
1
2
>>> print s
Hello, 世界

解决方案

  1. 永远记住 str 其实是 bytes,字符串应该用 unicode,尤其是包含中文时
  2. 如果能说服你的老板和同事,尽快把 Python 2 升级到 3

最后,如果你需要写出兼容 Python 2\3 的程序,这篇文档可以给你一些帮助。