如果想搞清楚 Python 的字符编码那么首先要明白计算机的字符编码如 ASCII、Unicode、UTF-8 等等字符编码之间的关系;还有字符编码的组成以及作用,看这篇文章可以
想要彻底搞清楚编码问题,我们必须要先搞清楚计算机是怎么存储数据的,这就涉及到了计算机基础的几个概念了,开篇我们就先来捋捋这几个容易混淆的概念。
bit
二进制位, 是计算机内部数据储存的最小单位,11010100 是一个 8 位二进制数。一个二进制位只可以表示 0 和 1 两种状态(2^1);两个二进制位可以表示 00、01、10、11 四种(2^2)状态;三位二进制数可表示八种状态(2^3)……
Byte
字节,是计算机中数据处理的基本单位,计算机中以字节为单位存储和解释信息,规定一个字节由八个二进制位构成,即1个字节等于 8 个比特(1Byte=8bit)。八位二进制数最小为 00000000,最大为 11111111;通常 1 个字节可以存入一个 ASCII 码,2 个字节可以存放一个汉字国标码。
字
在计算机中,一串数码作为一个整体来处理或运算的,称为一个计算机字,简称宇。字通常分为若干个字节(每个字节一般是8位)。在存储器中,通常每个单元存储一个字,因此每个字都是可以寻址的。字的长度用位数来表示。在计算机的运算器、控制器中,通常都是以字为单位进行传送的。
字长
字长,电脑技术中对 CPU 在单位时间内(同一时间)能一次处理的二进制数的位数叫字长。所以能处理字长为 8 位数据的 CPU 通常就叫 8 位的 CPU。同理 32 位的 CPU 就能在单位时间内处理字长为 32 位的二进制数据。
字节和字长的区别:由于常用的英文字符用 8 位二进制就可以表示,所以通常就将 8 位称为一个字节。字长的长度是不固定的,对于不同的 CPU、字长的长度也不一样。8 位的 CPU 一次只能处理一个字节,而 32 位的 CPU 一次就能处理 4 个字节,同理字长为 64 位的 CPU 一次可以处理 8 个字节。
常见的编码
ASCII
:1个字节,只编码英文字母和符号。
gb2312
:2个字节,增加了中文汉字和符号。
unicode
:把所有语言都统一到一套编码里,是所有字符对应二进制的集合,一般是2个字节,生僻字4个字节。
utf-8
: 可变长编码,常用的英文字母被编码成 1 个字节,汉字通常是 3 个字节,只有很生僻的字符才会被编码成 4-6 个字节。如果你要传输的文本包含大量英文字符,用 UTF-8 编码就能节省空间。
在计算机内存中,统一使用 Unicode 编码,当需要保存到硬盘或者需要传输的时候,就转换为 UTF-8 编码,这样可以节省很多存储空间。这就是由于两者各种优缺点,在不同场景使用不同的编码。
Note
Unicode 包容万国,优点是字符->数字的转换速度快,缺点是占用空间大。UTF-8 精准,对不同的字符用不同的长度表示,优点是节省空间,缺点是字符->数字的转换速度慢,因为每次都需要计算出字符需要多长的 Bytes 才能够准确表示。内存中使用的编码是 Unicode,用空间换时间,为了快,因为程序都需要加载到内存才能运行,因而内存应该是尽可能的保证快。硬盘中或者网络传输用 UTF-8,网络 I/O 延迟或磁盘 I/O 延迟要远大与 UTF-8 的转换延迟,而且 I/O 应该是尽可能地节省带宽,保证数据传输的稳定性。因为数据的传输,追求的是稳定,高效,数据量越小数据传输就越靠谱,于是都转成 UTF-8 格式的,而不是 Unicode。
关于字符集和编码,值得一看。
Python 2 中 str 与 unicode
因为 Python 的诞生比 Unicode 标准发布的时间还要早,所以最早的 Python 只支持 ASCII 编码,普通的字符串 ‘ABC’ 在 Python 内部都是 ASCII 编码的。
1 2 3 4 |
# Python 2.7 >>> import sys >>> sys.getdefaultencoding() 'ascii' |
Python 提供了 ord() 和 chr() 函数,可以把字符和对应的二进制相互转换。
1 2 3 4 |
>>> ord('a') #将字符转换为ASCII码对应的二进制表示方式 97 >>> chr(97) #将ASCII码对应的二进制转换为字符表示方式 'a' |
在 Python 2 里,字符串只有两种类型,unicode 和 str 。默认将 str 保存为 bytes 类型,而不是 Unicode(Python 3 已经把 str 保存为 unicode)。
- unicode string(unicode 类型):以 Unicode code points 形式存储,人类认识的形式,就是中文或英文。
- byte string(str 类型):以 byte 形式存储,机器认识的形式。
当我们直接使用双引号或单引号包含字符的方式来定义字符串时,就是 str 字符串对象,比如这样:
1 2 3 4 5 6 7 8 9 10 11 |
# Python 2 >>> str_obj = "中文" >>> type(str_obj) <type 'str'> >>> isinstance(str_obj, bytes) True >>> isinstance(str_obj, str) True |
可以看到 Python 2 中 str 等同于 bytes 类型。
Python 后来添加了对 unicode 的支持,而当我们在双引号或单引号前面加个 u
,就表明我们定义的是 unicode 字符串对象,比如这样:
1 2 3 4 5 6 7 8 9 10 |
>>> unicode_obj = u"中文" >>> type(unicode_obj) <type 'unicode'> >>> isinstance(unicode_obj, bytes) False >>> isinstance(unicode_obj, str) False |
在 Python 2 中,str 和 unicode 都是 basestring 的子类,basestring 有以下两个方法:
encode()
: 将 unicode 字符串对象转换(编码)为字节序列(Python 2 就是 str 对象),参数为转换后的编码。
decode()
: 将字节序列(Python 2 就是 str 对象)转换(解码)为 unicode 字符串对象,参数为转换前的编码。
看下面例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# 将 Unicode 字符串对象转换为 utf-8 和 gbk 字符串对象 >>> s = '中国' >>> print(type(s), s, len(s.encode('utf-8')), s.encode('utf-8'), len(s.encode('gbk')), s.encode('gbk')) (<type 'str'>, '\xe4\xb8\xad\xe5\x9b\xbd', 6, '\xe4\xb8\xad\xe5\x9b\xbd', 4, '\xd6\xd0\xb9\xfa') # 将 Unicode 字符串对象转换为 utf-8 和 gbk 字符串对象 >>> u = u'中国' >>> print(type(u), u, len(u.encode('utf-8')), u.encode('utf-8'), len(u.encode('gbk')), u.encode('gbk')) (<type 'unicode'>, u'\u4e2d\u56fd', 6, '\xe4\xb8\xad\xe5\x9b\xbd', 4, '\xd6\xd0\xb9\xfa') # 将 utf-8 编码的字符串对象转换为 Unicode 字符串 >>> print(s.decode('utf-8')) 中国 # 将 utf-16 编码的字符串对象转换为 Unicode 字符串 >>> print(s.decode('utf-16')) 룤붛 |
Python 2 中定义的一个 str 变量实则是字节串,那么这个字节串是什么编码格式的呢?这取决于终端默认编码。从上面的结果可以看出当我们在终端定义 s = '中国'
时,默认是 utf-8 编码格式(Linux 系统默认是 utf-8 编码,Windows 系统默认是 gbk 编码)。
当我们 decode 或 encode 时不指定编码,Python 2 默认就是使用 ASCII 码来进行编解码,就是系统默认编码(sys.getdefaultencoding 的返回值)。
从上面的结果可以看出 utf8 编码中一个中文字符是 3 个字节(一个英文字母是 1 个字节)。而 Unicode 才是真正意义上的字符串,由字符组成,其中u'\u4e2d'
就是 Unicode 字符集中字符的编号,这里就是对应的中
字,对字符编号进行编码就是字符编码要做的事情了。比如,这里 utf8 编码就把u'\u4e2d'
编号编码为'\xe4\xb8\xad
字节序列,转换机制如下:
u'\u4e2d'
转换为二进制为1001110 00101101
\xe4\xb8\xad
转换为二进制为11100100 10111000 10101101
使用 utf8 编码时,Unicode 字符集字符的编号对应的二进制是用来确认这个字符应该采用几个字节来进行编码。知道了用几个字节来进行编码后,就可以套用 utf8 的转换规则(套用模板)进行转换即可。
这里简单说一下,在 utf8 中会根据 Unicode 字符编码的有效码位来决定采用几个字节进行编码,比如英文就是 1 个字节,中文是 3 个字节等。这里 u'\u4e2d'
的有效码位(也就是二进制)是 15 位,utf8 会采用 3 字节编码模板(1110xxxx 10xxxxxx 10xxxxxx
)进行转换,保持模板前缀不变,把字符编号二进制填到非前缀位上,高位用 0 补足即可。1001110 00101101
经过套用 3 字节编码模板 1110xxxx 10xxxxxx 10xxxxxx
后就为 11100100 10111000 10101101
。其他字节对应的模板是什么这类更加详细的转换规则看 utf8 编码机制就行了。
对 Unicode 字符编号的不同编码方式所带来的的字节序列可能是不同,相同的就是为了兼容。可以看到'\xe4\xb8\xad\xe5\x9b\xbd'
bytes 串使用 utf-16 编码转换为 Unicode 字符串后成为 3 个字符,因为 utf-16 编码与 utf-8 编码方式不同,所以映射出不一样的字符。如果你使用 gbk 或其他编码来读取这个 bytes 串,可能还会报错。
Note
我们写程序向屏幕写东西本身就是调用的操作系统提供的写函数,C 标准里有 write 函数用来写,这是一次系统调用,传给该函数的参数本质就是个字节串。所以解释器看到 u’\uXXXX’ 这样的字符串,就把 XXXX 存到内存的某个地方,向输出设备输出的时候,直接调用操作系统提供的函数,把 XXXX 交给它,最终展示对应的字符。
不同编码转换,使用 Unicode 作为中间编码。
1 2 3 4 5 6 7 |
>>> s = '中文' >>> s.decode('utf-8').encode('gbk') '\xd6\xd0\xce\xc4' >>> s.decode('utf-8').encode('utf-8') '\xe4\xb8\xad\xe6\x96\x87' >>> s.decode('utf-8').encode('utf-16') '\xff\xfe-N\x87e' |
Python 3 中 str 与 bytes
在 Python 3.x 版本中,把'xxx'
和u'xxx'
已经都统一成 Unicode 编码了,即写不写前缀 u 都是一样的,str 也会被以 Unicode 形式保存新的内存空间中,str 可以直接 encode 成任意编码格式。而以字节形式表示的字符串则必须加上 b 前缀b'xxx'
。所以在 Python 3 中自然移除了 decode 方法,字符串本身就是 Unicode 了,所以不需要进行解码操作了。
所以,在 Python 3 中,字符串也是有两种类型 ,str 和 bytes。
- unicode string(str 类型):以 Unicode code points 形式存储,人类认识的形式,就是中文或英文。
- byte string(bytes 类型):以 byte 形式存储,机器认识的形式。
在 Python 3 中你定义的所有字符串,都是 unicode string 类型,使用 type 和 isinstance 可以判别。
1 2 3 4 5 6 7 8 9 10 11 |
# Python 3 >>> s = '中' >>> type(s) <class 'str'> >>> isinstance(s, str) True >>> isinstance(s, bytes) False |
而 bytes 是一个二进制序列对象,你只要你在定义字符串时前面加一个 b
,就表示你要定义一个 bytes 类型的字符串对象。
1 2 3 |
>>> b = b'hello' >>> type(b) <class 'bytes'> |
但是在定义中文字符串时,你就不能直接在前面加 b
了,不支持,而应该使用 encode
转一下。
1 2 3 |
>>> b = '中' >>> b.encode('utf-8') b'\xe4\xb8\xad' |
可以看出在 Python 3 中 bytes 就是一个显示的字节序列了。
那么在 Python 3 的编码和解码,其实就是 Unicode str 与 bytes 的相互转化的过程。
encode()
: 将 unicode 字符串对象转换(编码)为字节序列,参数为转换后的编码。
decode()
: 将字节序列转换(解码)为 unicode 字符串对象,参数为转换前的编码。
看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# 定义 unicode str 类型 >>> s = '中文' >>> type(s) <class 'str'> # 将 unicode str 进行 utf-8 或 gbk 编码 >>> print(s.encode('utf-8')) b'\xe4\xb8\xad\xe6\x96\x87' >>> print(s.encode('gbk')) b'\xd6\xd0\xce\xc4' # 将对应编码后的二进制序列解码成 unicode str >>> print(b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')) 中文 >>> print(b'\xd6\xd0\xce\xc4'.decode('gbk')) 中文 |
可以看出 Unicode 可以进行转换为任意其他编码。但编码后的二进制序列必须使用对应的编码进行解码,不然就乱码了。
比如我们把 utf-8 编码的二进制序列解码为 utf-16 编码,如下:
1 2 |
>>> print(b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-16')) 룤螖 |
由于 Python 3 字符串默认就是 Unicode,所以可以直接输出为 Unicode 字符:
1 2 3 4 5 6 7 |
# Python 3 >>> s = '中' >>> s '中' >>> print("\u4e2d") 中 |
设置文件编码
在 Python 2 中,默认使用的是 ASCII 编码来读取的,因此,我们在使用 Python 2 的时候,如果你的 Python 文件里有中文,运行是会报错的。原因就是 ASCII 编码表太小,无法解释中文。而在 Python 3 中,默认使用的是 uft-8 来读取,所以省了不少的事。
对于这个问题,通常解决方法有两种:
由于 Python 源代码也是一个文本文件,所以当你的源代码中包含中文的时候,在保存源代码到磁盘时,就需要指定读取的编码格式。当 Python 解释器读取源代码时,会按照你指定的编码进行读取,我们通常在文件开头写上如下行:
1 |
# -*- coding: utf-8 -*- |
注释是为了告诉 Python 解释器,按照 UTF-8 编码格式读取源代码到内存。否则,Python 2 中默认使用 ASCII,你在源代码中写的中文输出就会乱码。Python 3 中默认使用 utf-8。
另外也可以使用 setdefaultencoding 方法。
1 2 3 4 |
import sys reload(sys) sys.setdefaultencoding('utf-8') |
这里在调用 sys.setdefaultencoding(‘utf-8’) 设置默认的解码方式之前,执行了 reload(sys),这是必须的,因为 Python 在加载完 sys 之后,会删除 sys.setdefaultencoding 这个方法,我们需要重新载入sys,才能调用 sys.setdefaultencoding 这个方法。
<参考>
https://segmentfault.com/a/1190000007309014
https://mp.weixin.qq.com/s/6vFJoafikGm01FOWZgYVoQ
https://mp.weixin.qq.com/s/ImVH-XZk5RyjT8D7aMCmZw