Python 2 字符串编码踩坑小结
Tips: 如果您已经充分理解问题是什么,请直接跳到 #问题出在哪里 一节。
字符串和编码
先从概念说起,字符串和它的编码是两个不同的概念:
- 字符串是一段文字本身,可以是中文可以是英文,以及各种语言
- 字符串的编码是计算机存储、处理字符串的方式;作为一种数据,它和其他数据一样,都是以一串0和1组成的,通常我们用字节数组来表示它。
字符串经过编码(encode) 就成为了一堆数据,反过来,数据经过解码(decode) 就变回我们认识的字符串。
从 Python 3 说起
这个编码问题(坑)可以说是 Python 2 被吐槽最多的黑点,没有之一。为了防止上来就掉进 Python 2 的坑里,我们先来看看 Python 3 里“改进”后是什么样子的。
1 | "Hello, 世界" s = |
哈,没有任何问题!(数长度的时候别漏了空格)
查阅文档,我们发现 str
有个函数叫
encode()
,它看起来很眼熟,让我们来试试:
1 | "utf-8") b = s.encode( |
这个 b''
的前缀表示返回值是一个 bytes
变量,也就是一堆数据了。
为什么这里面"Hello"还是原来的样子,但是“世界”变成一坨
\x??
了?这是因为 ASCII 实在太有名了,程序员们都看得懂:这个
H
其实表示的是一字节0x48
。而后面“世界”的编码不在 ASCII 的编码范围内,所以只能用\x??
表示了。这样看起来也许更清晰一些:
1
2 hex() b.
'48656c6c6f2c20e4b896e7958c'
有 encode()
当然也有
decode()
。我们对刚刚拿到的 bytes
b
解码,果然会变成原来的字符串。
1 | 'utf-8') b.decode( |
OK,现在你已经明白了奥义所在,是时候去踩坑了。
Python 2 的世界
初见茅庐
先来一道开胃菜:
1 | "Hello, 世界" s = |
▲ 为什么这个长度是 13 ?明明是 9 个字符啊!
1 | s |
▲ s
你怎么坏掉了?
1 | 'utf-8') b = s.encode( |
▲ 我可能用了假的 encode()
1 | 'utf-8') b.decode( |
▲ 喵喵喵?
以上,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 | u'Hello, 世界' s = |
至于为什么 str
也有
encode()
,主要是为了尽可能保持和 Python 3
的兼容性,以让部分程序能在 2、3
同时运行。于是事情变得更糟糕了。
原来如此
现在我们可以解释刚刚遇到的奇怪情况了:
- “为什么这个长度是 13 ?明明是 9 个字符啊!”——因为 Python 自动帮你编码了,编码后是 13 个字节,常见的汉字在 UTF-8 编码下为 3 个字节
- “
s
你怎么坏掉了?” ——str
本来就是字节数组 - “我可能用了假的
encode()
”——你不应该对str
变量做encode
,它本来就是编码后的 - “喵喵喵?”——这是正常的,只是因为 Python 2 没有把 Unicode 字符显示成中文字符,用 print 就没问题了:
1 | print s |
解决方案
- 永远记住 str 其实是 bytes,字符串应该用 unicode,尤其是包含中文时
- 如果能说服你的老板和同事,尽快把 Python 2 升级到 3
最后,如果你需要写出兼容 Python 2\3 的程序,这篇文档可以给你一些帮助。