Python 中的字节序列与编码

阅读数:115 2019 年 9 月 25 日 08:00

Python 中的字节序列与编码

编码问题在开发中经常遇到,为什么经常一遇到中文就会出现编码问题?我该如何指定编码才不会出现问题?Python 对编码的处理是怎样的?为了搞清楚编码问题和方便解决编码 bug,谨以此篇,纪念那些年调过的编码 bug。

1 理清基本概念

1)字节(Byte) 是一串二进制序列,1 Byte = 8 Bit(二进制位);字节序列就是连续的多个字节,同理,下文中出现的二进制序列也是指将字节序列换算成二进制的序列。

2)字符(Character)是一个信息单位。对使用字母系统或音节文字等自然语言,它大约对应为一个音位、类音位的单位或符号。简单来讲就是一个汉字、假名、韩文字……,或是一个英文、其他西方语言的字母。

3)ASCII (American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套计算机编码系统。它主要用于显示现代英语,而其扩展版本 EASCII 则可以部分支持其他西欧语言,并等同于国际标准 ISO/IEC 646。

标准 ASCII 以 1 字节(8 Bit) 为单位对字符进行编码,共包含 33 个无法显示字符(多为控制字符)和 95 个可显示字符,共 128 个。

IBM 对标准 ASCII 做过一种非标准扩展,即“扩展字符集”, 最终可以将编码的字符数量扩充到 256 个,即达到了 8 Bit 编码的上限。

4)Unicode 是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得计算机可以用更为简单的方式来呈现和处理文字。

Unicode 编码方式为: U+hhhh, 其中 h 代表一个 16 进制数字,理论上需要占用 2 个字节的空间,但基于各种扩展考虑用 4 个字节来表示一个字符。

Unicode 的实现方式不同于编码方式。一个字符的 Unicode 编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对 Unicode 编码的实现方式有所不同。Unicode 的实现方式称为 Unicode 转换格式(Unicode Transformation Format,简称为 UTF)

可以认为 UTF-8 是 Unicode 标准的一种实现方式。

2 为什么会有上述概念

2.1 方便指挥计算机的运行

目前的计算机只能存储和处理二进制序列,因为字节是计算机数据结构的重要组成单位,因此也可以视为字节序列;而人类读写文本最方便的是字符串序列;因此,为了方便人类与计算机的对话,必需约定某种二进制序列到字符串序列的转换规则,即编码标准,包括 ASCII,UTF-8 等。比如在 UTF-8 标准中,‘’11101000 10110100 10011101” 代表了中文“贝”, “11100101 10100011 10110011” 代表了 “壳”, 遗憾的是 ASCII 并不支持中文编码,即无法用 ASCII 方式将中文翻译成二进制序列。

2.2 标准的制定不是一蹴而就的

由于计算机的发展并不是一开始就面向全世界的,编码规范也是经历了各种版本的各种修改。

ASCII 对中文不适用,那是自然啦,毕竟当初仅仅是在美国使用。全球化之后,计算机技术作为重要的输出,ASCII 显得力不从心了,因为它规定了一个字符必须由 8 bit(1 Byte) 来表示,即最多表示 256 个字符, 面对大千世界的无数字符,ASCII 标准无法胜任,然后就有了后来的 Unicode。

2.3 Unicode 的故事

统一码联盟在 1991 年首次发布了 The Unicode Standard。Unicode 的开发结合了国际标准化组织所制定的 ISO/IEC 10646,即通用字符集。但这时候的 Unicode 还不支持中文。Unicode 完全兼容 ASCII, 这也是全英文的程序不容易出现编码问题的原因。

1992 年 6 月新的 Unicode 标准中添加了“中日韩统一表意文字”, 中文开始出现在 Unicode 中。有意思的是这里边很多韩语、日语中的汉字与汉语中起源相同、本义相同、形状一样或稍异的表意文字编码是一样的。

最近一版 Unicode 标准是 2018 年 6 月发布的。

3 编码无处不在

要想跟计算机交流,必须经过编码这一关。从用键盘敲出一个字符开始,编码工作就开始了!我们能看到屏幕上的字符只是强大的 UI 系统为了方便人的认知,背后计算机所记下的都是一串串的二进制序列。想想早期计算机的穿孔纸带,那种交互才是噩梦。所以不要以为机器懂你输入的字符理所当然,背后经过了多少人的努力和计算机不知疲倦地帮你翻译。

开发过程中出现编码问题的地方可以分为以下几种:

1)终端或编辑器

这里出现的编码问题最有迷惑性。我们在终端或者编辑器里输入一个字符,显示出来也是跟我们认知一样的字符;但在其背后已经被翻译成了计算机语言,即经过了编码,成为了字节序列。这也是我们经常 debug 时遇到的困扰:明明在终端显示正常,怎么就执行不通过!

2)源码

源码无疑会被计算机存储下来,用任何一种编辑器进行开发时都会经过编码才会成为计算机理解的二进制序列。所有总会有默认或指定的编码格式。

Python2 文件开头的

复制代码
1# coding=utf-8

就是告诉解释器,源码文件中的所有字符都是以 utf-8 格式编码的。(下文中所有未注明编码格式的 python2 代码均指定了 utf-8 编码)

3)数据

同源码文件,数据文件也会涉及编码,但普通的文本文件没有编码声明标准,因此要想使用非默认编码也就必须要在程序中指定编码格式。

4)变量

我们遇到问题最多的情况应该就是发生在变量的编码上。从编码角度来看,变量的值来源可以使源码,也可以是文件,也就是变量可能与源码文件或数据文件有关。我们遇到的大部分编码问题都会出现在变量上,而且不易觉察。

4Python2 中更容易出现编码问题

1) Python2 中的 str 类型与 bytes 类型几乎完全一样,一样到 bytes 只是 str 类重新命名了。

复制代码
1str_s = str(" 贝壳 ")
2bytes_s = bytes(" 贝壳 ")
3print(str_s)
4print(bytes_s)
5print(str_s == bytes_s)
6print(type(str_s) == type(bytes_s))
7print(len(str_s))
8print(len(bytes_s))

输出:

复制代码
1 贝壳
2 贝壳
3True
4True
56
66

str 和 bytes 类型比较时也完全一样,并且长度都是指其占的字节数量。

注意:print 函数调用的是 str 和 bytes 类的 __ str__() 函数,最终输出的是字符;而如果调用各自的 __repr__() 函数,将会输出字节序列。

复制代码
1str_s = str(" 贝壳 ")
2bytes_s = bytes(" 贝壳 ")
3str_sequence = (str_s.__repr__())
4str_chars = (str_s.__str__())
5bytes_sequence = (bytes_s.__repr__())
6bytes_chars = (bytes_s.__str__())
7print(str_sequence)
8print(str_chars)
9print(bytes_sequence)
10print(bytes_chars)

输出:

复制代码
1'\xe8\xb4\x9d\xe5\xa3\xb3'
2 贝壳
3'\xe8\xb4\x9d\xe5\xa3\xb3'
4 贝壳

可以看出,python 2 str 类型的操作实际上是 对字节序列的操作,那么就会严重依赖编码格式的一致,否则即使不报异常也可能导致数据处理错误!

而且,不要轻信 python 2 中 print 出来的字符串,那是经过翻译的字符。

PS: 仔细观察可以看出“贝壳”两个汉字使用了了 12 个 16 进制的数字表示,即占了 6 个字节. 比 Unicode 标准的 8(4 * 2)个字节少了两个字节,这也是 UTF-8 优化存储空间的体现。

  1. unicode 类型

Python 2 中有单独的 unicode 类,用于处于 Unicode 标准下的字符。声明该类型时只需在字符串前加 ‘u’。

复制代码
1unicode_s = u'贝壳'
2print(type(unicode_s))
3print(unicode_s)
4print(len(unicode_s))

输出:

复制代码
1<type 'unicode'>
2 贝壳
32

注意这里的长度是 2, 指的是字符数量。

若要比较同字符代表的 unicode 与 str 或 byte 是否相等会报 UnicodeWarning, 但会继续执行,结果是 False。

复制代码
1str_s = str(" 贝壳 ")
2bytes_s = bytes(" 贝壳 ")
3unicode_s = u'贝壳'
4print(type(unicode_s) == type(str_s))
5print(type(unicode_s) == type(bytes_s))
6print(unicode_s == str_s)
7print(unicode_s == bytes_s)

输出:

复制代码
1UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal
2 print(unicode_s == str_s)
3UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal
4 print(unicode_s == bytes_s)
5False
6False
7False
8False

3)默认编码

python 2 默认的源码编码格式是 ASCII, 在全英文的代码中不会出现问题,但如果有超过标准 ASCII 的 128 个字符范围的字符,解释器就会报错。

复制代码
1import sys
2print(sys.getdefaultencoding())

输出:

复制代码
1ascii

5 最佳实践

既然有了统一的编码标准,那么大家都依照同一个标准,然后将标准交给语言底层实现不就可以了?即我们只想处理字符 (因为 unicode 与字符是一一对应的,也可以认为是 unicode),不要让我们困扰于编码问题。

因此 Unicode 三明治原则就被提了出来:

1)要尽早地将输入的字节序列解码成字符串;

2)中间过程不要进行编解码,专心处理字符串;

3)输出时要尽量晚地编码成字节序列。

Python 3 中很巧妙地遵循了这个原则(本部分代码均为 python 3 环境):

  1. python 3 默认 UTF-8 编码, 不再需要显式地指定源码的编码格式就可以支持所有字符,而不仅仅限于英文。
复制代码
1import sys
2print(sys.getdefaultencoding())

输出:

复制代码
1utf-8

2)python 3 str 类型内部实现不再是字节序列,而是 Unicode

复制代码
1str_s = str(" 贝壳 ")
2bytes_s = bytes(" 贝壳 ", encoding='utf-8')
3unicode_s = u'贝壳'
4print(type(str_s), type(bytes_s), type(unicode_s))
5print(len(str_s), len(bytes_s), len(unicode_s))
6print(str_s == bytes_s)
7print(str_s == unicode_s)
8print(bytes_s == unicode_s)
9print(str_s.__str__())
10print(str_s.__repr__())
11print(bytes_s.__str__())
12print(bytes_s.__repr__())
13print(unicode_s.__str__())
14print(unicode_s.__repr__())

输出:

复制代码
1<class 'str'> <class 'bytes'> <class 'str'>
22 6 2
3False
4True
5False
6 贝壳
7'贝壳'
8b'\xe8\xb4\x9d\xe5\xa3\xb3'
9b'\xe8\xb4\x9d\xe5\xa3\xb3'
10 贝壳
11'贝壳'

Python 3 中,u’xxx’ 表示的不再是 unicode 类型,而是 str(其实 python 3 中已没有 unicode 类). 而且 str 的长度即字符的数目,更贴近“所见即所得”,方便认知。而且要想处理字节序列,还是有 bytes 可以用的。用 bytes 的时候才需要注意编码问题,因此 bytes 实例化必须要有 encoding 参数来指定编码格式。

6BOM

在某些可以指定编码格式的编辑器里经常可以看到如 “有 BOM UTF-8 编码” 和 “无 BOM UTF-8 编码”(如 notepad++) 的编码格式。这里的 BOM 是什么意思呢?为什么 UTF-8 会有不止一种编码?

BOM,即字节序标记(byte-order mark),位于字节流的开头,用于标记此字节流的字节序。

BOM 源于在 UTF-16 中,2 字节作为 " 一组 " 来存储,存在大尾和小尾两种字节序,因此需要 BOM 来指定是哪种字节序。

对于 UTF-8,是以 1 字节为“一组”来存储的,也就不存在大小尾的问题,但它仍然支持了 BOM,UTF-8 中 BOM 占字节流开头的三个字节。这里 BOM 的作用仅仅用来标明这是一个 UTF-8 字节序列。

7 建议

由于很多同学还在使用 Python2.7,较容易出现编码问题,所以个人有几个小建议:

1)不要把 str 当做字符串,而是字节序列,事实上也是这样。

2)出现问题的地方先确定是字节序列还是 unicode.

  1. 除非必要使用字节序列,遵循 Unicode 三明治原则,以 unicode 类型处理逻辑。

  2. 读取数据出现编码问题要观察数据内容。经常有数据中出现 \u 开头的 unicode 字符串形式,比如文件中存储的就是“\u8d1d\u58f3”这样的字符。那解码成 unicode 时需要指定编码格式为 Python 特有的 "unicode-escape" 格式,可以识别这些 unicode 值, 而不是当做 ‘’, ‘u’, ‘8’, ‘d’, ‘1’, ‘d’, … 组成的字符串。

复制代码
1str_a = "\u8d1d\u58f3".decode("utf-8").encode("utf-8")
2str_b = "\u8d1d\u58f3".decode("unicode-escape").encode("utf-8")
3print(str_a)
4print(str_b)
5print(len(str_a))
6print(len(str_b))

输出:

复制代码
1\u8d1d\u58f3
2 贝壳
312
46

作者介绍:
行者(企业代号名),目前负责贝壳找房房源方向的策略算法工作。

本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。

原文链接:

https://mp.weixin.qq.com/s/Hugy5lt_hyzUef3ttM7gBQ

评论

发布