一文说清楚如何处理 Python 的编码

用 Python 久了,总会遇到与中文编码相关的问题,不管是打印的时候出现乱码,还是直接报错。而我之前一直是不断使用 encodedecode 进行尝试,今天实在受不了了,彻底摸清楚了,遂整理了一番。

编码种类以及起源

自行了解

从 python 2.x 说起

str 与 unicode

什么是 str,什么是 unicode

首先,strunicode 都是 basestring 的子类。
那么,我们怎么知道一个变量里到底是 str 还是 unicode 呢?

Python 提供了 isinstance 函数:

  1. 判断 string 是否为 strisinstance(string, str)
  2. 判断 string 是否为 unicodeisinstance(string, unicode)

以及 type 函数:

1
2
3
4
5
In [3]: type("中文")
Out[3]: str

In [4]: type(u"中文")
Out[4]: unicode

那么,我们常说的 字符串,到底哪个呢?我认为,看个人。如果你和我一样觉得直接写在源代码里的(如 a = "123")就叫字符串,那么 str 就是字符串。如果你觉得 字符串 是由 多个字符 串起来 的,那么 unicode 才是字符串。

也就是说,unicode 由 多个字符 组成,比如:

1
2
3
4
5
In [1]: len(u'中文')
Out[1]: 2

In [2]: len('中文')
Out[2]: 6

'中文' 这两个字符组成。明显 unicode 更符合我们求长度的逻辑。

总结:不管怎么样,对字符串的定义,选一个自己认为合理的,这个无关紧要。我倾向于前者,所以下面默认 字符串 就是 str

创造 str 与 unicode

直接创造 str 与 unicode 的方法都很简单,直接写在源码里:

  1. str: a = "123"
  2. unicode: a = u"123"

还有那些地方会创造出 str 与 unicode 呢?比如读/写文件,从远程获取(爬虫或 socket)等等。

str2unicode,unicode2str

Python 提供了 decode(解码)/encode(编码) 用于两者的转换:

  1. str -> decode("某种编码") -> unicode
  2. unicode -> encode("某种编码") -> str

总结:unicode 经过编码后形成 strstr 经过解码后形成 unicode

至于如何选择正确的 "某种编码",下面会说。

如何正确地编码/解码

环境编码

系统默认编码

  1. Linux,Unix:utf8
  2. Windows:gbk

还有输出时,打印到终端的编码

编辑器的编码

不管是 IDE 还是 atom 记事本,打开源代码的时候总会按照某一特定的编码打开。基本上,这个编码总是可以设置的,如 atom:

保证源代码的文件编码,为避免乱码以及后续处理的方便,最好统一设置为 utf8

文件头部编码声明

即 python 解释器读取源代码时的编码。

python 解释器读取源代码时默认编码是 ASCII。在源代码文件中,如果用到非 ASCII 字符(如中文),需要在文件头部进行编码声明。否则会报错:

1
2
File "test.py", line 8
SyntaxError: Non-ASCII character '\xe6' in file test.py on line 9, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

声明编码方式为:# -*- coding: utf-8 -*-#coding=utf-8-*- 实际上没啥用,只是为了...好看...)。对 Python 来说,无所谓你写的是 utf-8 还是 utf8,它都能认识。

注意,声明编码必须放在源代码的第一行

假设代码中有:a = '中文'。若头部声明 utf-8, 则 a 的编码类型为 utf-8;若头部声明 gbk,则 a 的编码类型为 gbk。即,代码中直接声明的字符串编码类型,与此直接相关。

所以,为了避免乱码以及后续处理的方便,最好统一头部声明为 utf8

如何选择编码类型

现在我们知道了:常见编码类型有: ascii, utf8, gbk。

先做几个小实验

  1. 文件头部声明对字符串编码类型的影响(假设声明为 utf8):
  2. # -*- coding: utf8 -*- -> "任意字符串" -> utf8
  3. # -*- coding: utf8 -*- -> u"任意字符串" -> unicode
  4. unicode 的 encode/decode:
  5. 纯 ascii 的 unicode -> encode("编码类型 A") -> 字符串
  6. 含中文的 unicode -> encode("任意编码类型") -> 字符串
  7. 纯 ascii 的 unicode -> decode("任意编码类型") -> unicode
  8. 含中文的 unicode -> decode("任意编码类型") -> 'ascii' codec can't encode characters in position 1-32: ordinal not in range(128)
  9. 字符串的 encode/decode:
  10. 纯 ascii 的字符串 -> decode("任意编码类型") -> unicode
  11. 含中文的字符串 -> decode("utf8") -> unicode
  12. 含中文的字符串 -> decode("非 utf8") -> 乱码 或 UnicodeDecodeError: '非 utf8' codec can't decode bytes in position 117-118: illegal multibyte sequence
  13. 纯 ascii 的字符串 -> encode("任意编码类型") -> 字符串
  14. 含中文的字符串 -> encode("任意编码类型") -> UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 115: ordinal not in range(128)

文件头部声明为 gbk 时同理。

于是,我们可以得出结论:

  1. 除非有信心代码不会出现中文(包括硬编码或者数据流)之外,头部一定要声明编码,且最好为 utf8
  2. ascii 编码类型,随便你怎么搞都没事。
  3. 最好,不要对 str 使用 encode,不要对 unicode 使用 decode
  4. 编码之间的转换怎么处理呢?用 unicode 搭桥
    1
    2
    3
    4
    5
    # -*- coding: utf8 -*-
    string = "我"
    print string.decode('utf8').encode('gbk')
    # 先解码为 unicode,再编码为 gbk
    # 在终端字符设置为 utf8 时,输出为乱码

网上的一些说法

利用 sys 解决。方法如下:

在源代码处添加:

1
2
3
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

那么,这个方法的原理是什么呢?

在 python 中,对 str 使用 encode 时,其实是先解码为 unicode ,然后再编码的。但是,这个时候我们并没有声明编码类型,于是 Python 就用默认的编码类型:ascii。改变这一默认编码类型的方式就是上面的那段代码。

我们还可以做个小实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
In [1]: a = "我"
In [2]: a.encode()
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 0: ordinal not in range(128)

In [3]: a = u"我"
In [4]: a.decode()
UnicodeEncodeError: 'ascii' codec can't encode character u'\u6211' in position 0: ordinal not in range(128)

In [5]: import sys
...: reload(sys)
...: sys.setdefaultencoding('utf-8')
...:

In [6]: a = u"我"
In [7]: a.decode()
Out[7]: u'\u6211'

In [8]: a = "我"
In [9]: a.encode()
Out[9]: '\xe6\x88\x91'

显然这个方法并不彻底,说不定还会造成奇怪的 bug。所以最好别用。

不过话说回来,这个自动编码,有时候真是大坑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
In [1]: "我"+"我".decode("utf8")
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 0: ordinal not in range(128)

In [2]: "我"+u"我"
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 0: ordinal not in range(128)

In [3]: "我" in u"我"
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 0: ordinal not in range(128)

In [4]: str(u"我")
UnicodeEncodeError: 'ascii' codec can't encode character u'\u6211' in position 0: ordinal not in range(128)

In [5]: u"%s" % "我"
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 0: ordinal not in range(128)

In [6]: raw_input("编码")
编码
Out[7]: ''

In [8]: raw_input(u"编码")
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

......

也难怪很多人用这个方法了吧。。。

阶段总结

py2.x:

  1. 规范编码:环境编码 + IDE/文本编辑器 + 文件编码 + 数据库编码+...
  2. 头部编码声明
  3. 不要对 str 使用 encode,不要对 unicode 使用 decode
  4. 乱码/报错的时候,先搞清楚编码的类型,查找小实验中对应的情况,再进行对应的编码/解码
  5. 强烈建议统一用 utf8,不管网站也好,数据库也好,这是大势所趋。

痛改前非的 python 3.x

字符串?str?unicode?bytes?

py2.x 的非 ascii 编码/解码问题一直被吐槽这么多年后,py3.x 终于下定决心彻底解决这个问题。解决的方式:

文本总是 Unicode,由 str 类型表示,二进制数据则由 bytes 类型表示。Python 3 不会以任意隐式的方式混用 strbytes

所以,py3.x 首先统一了表示形式,然后不会再任意隐式的方式混用。

接下来解释一下:首先,Unicode 是内存编码表示方案(是规范),而 UTF 是如何保存和传输 Unicode 的方案(是实现)。 这也是 UTF 与 Unicode 的区别。py3.X 中只有一种能保存文本信息的数据类型:str,不可变,保存的是 Unicode 码位。Unicode 是离用户更近的数据,bytes 是离计算机更近的数据。

在 py2.x 中,我们曾经讨论过 什么是 str,什么是 unicode,以及字符串的定义问题,还举了一个例子,说明 unicode 在求长度的时候更符合逻辑。所以,py3.x 认为 unicode 才是 字符串,而在 Py2.x 中的 str,更像是 py3.x 中的 bytes

说得有点多,总结一下:

  1. py2.x
  • 字符串str 类型
  • type("123")str
  • len("我"):3
  • len("我".decode("utf8")):1
  1. py3.x
  • 字符串unicode 类型
  • type("123")str
  • len("我"):1
  • len("我".encode()):3
  1. 注意,str 表示的概念都是字符串类型,只不过 Py2.x 的 str 是经过编码的 unicode,而 py3.x 的 str 就是 unicode。 那么,py3.x 的 unicode 经过编码后,是什么呢?就是 bytes。也就是说,py3.x 的字符串默认是 unicode,至于你想把它怎么编码(utf8 或者 gbk),都是你自己的事。

说清楚 py3.x 的解决方案之后,我们再来看它的编码/解码。

编码/解码

对于 str 类型(即 unicode),只有一个 encode 方法将 字符串 转化为一个字节码(即 bytes),而且 bytes 也只有一个 decode 方法将字节码转化为一个文本字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
a = "我" # a 是一个 字符串,类型是 str,用 unicode 表达
print(a) # '我'

a = b'\xe6\x88\x91' # a 是一个 字节流,类型是 bytes,用 utf8 编码的 unicode 表达
print(a) # b'\xe6\x88\x91'

a = "我".encode("utf8") # 将 a 编码,转为用 utf8 编码的 unicode 表达
print(a) # b'\xe6\x88\x91'

a = b'\xe6\x88\x91'.decode("utf8") # 将 a 解码,转为用 unicode 表达
print(a) # "我"

a = "我".decode("utf8") # 试图将 unicode 解码
# 报错:AttributeError: 'str' object has no attribute 'decode'

a = b'\xe6\x88\x91'.encode("utf8") # 试图将 bytes 编码
# 报错:AttributeError: 'bytes' object has no attribute 'encode'

a = b'\xe6\x88\x91'.decode("gbk") # 解码的编码类型与原有编码类型不匹配
# 报错:UnicodeDecodeError: 'gbk' codec can't decode byte 0x91 in position 2: incomplete multibyte sequence

基本不会有乱码以及各种奇怪报错的问题了。算是彻底解决了。

阶段总结

py3.x:

  1. 规范编码:环境编码 + IDE/文本编辑器 + 文件编码 + 数据库编码+...
  2. 头部编码声明:不再必要
  3. 乱码/报错的时候,一定是解码的时候,解码的编码类型与原有编码类型不匹配。

谈谈兼容性

py2.x 与 3.x 如何进行正确地编码/解码都说好了,现在说说兼容性。回顾 py3.x 的解决方案,我们可以获得启示:默认使用 unicode,需要的时候再编码

Python 作出的努力

自从 py3.x 出世,以及官方声明不在对 2.x 进行更新后,兼容性一直是一个大问题。
对此,最新的 py2.7.15,支持 py3.x 的一些语法;而 3.x 也保留了一些 py2.x 的语法:

  1. py2.x:
  • a = b"123" # a 依然是 str 而不是 bytes。
  • 可以进行头部编码声明
  1. py3.x:
  • a = u"123" # a 本来就是 unicode
  • 可以进行头部编码声明

以上语法仅仅是为了兼容,不会起到它们原来的作用。

我们需要注意的地方

兼容性靠别人不如靠自己,写代码的时候不妨留意一点编码问题。

  1. 规范编码:环境编码 + IDE/文本编辑器 + 文件编码 + 数据库编码+...
  2. 声明文件头部编码
  3. 硬编码的时候加上 ua = u"我"
  4. 将 Python 代码当做一个杯子,将数据流倒进杯子的时候,一律解码转为 unicode。杯子里一律用 unicode 处理。将数据流倒出杯子的时候,编码为 utf8(当然,看具体使用场景)。
  5. 始终坚持使用 UTF-8 编码,始终坚持使用 UTF-8 编码,始终坚持使用 UTF-8 编码

来呀快活呀


一文说清楚如何处理 Python 的编码
https://www.tr0y.wang/2018/09/13/py-coding-type/
作者
Tr0y
发布于
2018年9月13日
更新于
2024年4月19日
许可协议