血案后对python3.7最新字节字符编码解码知识整理

之前因为工作上面的需要,查了好多字符串字节编码解码方面的资料,结果发现鱼龙混杂。本身由于Python3与Python2在这方面的改动很大,再加上从Python3开始,伴随着版本的迭代,一些方法也有了变化,很多以前的转换方法都不能用了,整个过程被折磨的脑壳疼,在此从自己本身的理解上面做个总结,以后方便自己来查阅。

1. 先从概念理解开始

某大佬云:人类使用文本,计算机使用字节序列

我们先可以看一下从ASCII到Unicode的发展历史,然后理解下面的概念。

  • bit:二进制位, 是计算机内部数据储存的最小单位,11010100是一个8位二进制数
  • byte:字节,是计算机中数据处理的基本单位,计算机中以字节为单位存储和解释信息,规定一个字节由八个二进制位构成,即1个字节等于8个比特(1Byte=8bit)。八位二进制数最小为00000000,最大为11111111;通常1个字节可以存入一个ASCII码,2个字节可以存放一个汉字国标码
  • Unicode:Unicode(统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求
  • 字符:“字符”的最佳定义是Unicode字符,字符的具体表述取决于所用的编码
  • 字符串:一个字符串就是一个字符序列(多个字符组成)
  • 码位:字符的标识,是0~1114111(这个数字记着,下面有用)的数字,在Unicode中以4-6个十六进制数字表示,而且加前缀“U+”,比如字母A的码位就是U+0041,欧元符号€的码位就是U+20AC
  • 编码:把码位转换成字节序列的过程就是编码
  • 解码:把字节序序列转换成码位的过程

简单看一个书本上例子当下酒菜:

1
2
3
4
5
6
7
8
9
10
>>> s = 'café'
>>> len(s) # ➊
4
>>> b = s.encode('utf8') # ➋
>>> b
b'caf\xc3\xa9' # ➌
>>> len(b) # ➍
5
>>> b.decode('utf8') # ➎
'café'

❶ ‘café’ 字符串有 4 个 Unicode 字符。
❷ 使用 UTF-8 把 str 对象编码成 bytes 对象。
❸ bytes 字面量以 b 开头。
❹ 字节序列 b 有 5 个字节(在 UTF-8 中,“é”的码位编码成两个字节)。
❺ 使用 UTF-8 把 bytes 对象解码成 str 对象

如果想帮助自己记住 .decode() 和 .encode() 的区别,可以把字节序列想成晦涩难懂的机器磁芯转储,把 Unicode 字符串想成“人类可读”的文本。那么,把字节序列变成人类可读的文本字符串就是解码,而把字符串变成用于存储或传输的字节序列就是编码。

2. 几个常会看到的函数

2.1 数字转换

bin(),oct(),int(),hex(),这些函数就不说了,数字转换手算算也是非常简单的。但是我们要知道Python中各种进制数据的表示,以十进制的23为例,二进制前缀为0b,0b10111,八进制前缀为0o,0o27,十六进制前缀为0x,0x17.

一个在转换进制的同时高位补零的小技巧:

1
2
3
4
5
6
7
8
9
>>> bin(2)
0b10'
>>> '{:08b}'.format(2)
'00000010'
>>> '{:8b}'.format()
' 10'
# 输出的都是字符串
>>> int('00000010',2)
2

2.2 不起眼的主角

chr(i)返回 Unicode 码位为整数 i ( 0 <= i <= 0x10ffff)的字符的字符串格式。例如,chr(97) 返回字符串 ‘a’,chr(8364) 返回字符串 ‘€’。这是 ord() 的逆函数

ord(c)对表示单个 Unicode 字符的字符串,返回代表它 Unicode 码点的整数。例如 ord(‘a’) 返回整数 97, ord(‘€’) (欧元符合)返回 8364 。这是 chr() 的逆函数。

所以上面chr函数里面i的范围最大为0x10ffff,转换成十进制就是码位概念里字符标识数字的1114111,欧元符号转换也是同理,0x20AC就是8364。

3. 计算机所能理解的字节

3.1 Python中的字节对象

操作二进制数据的核心内置类型是 bytes 和 bytearray。 它们由 memoryview 提供支持,该对象使用 缓冲区协议 来访问其他二进制对象所在内存,不需要创建对象的副本。
bytearray():返回一个新的 bytes 数组。 bytearray 类是一个可变序列,包含范围为 0 <= x < 256 的整数。它有可变序列大部分常见的方法
bytes():返回一个新的“bytes”对象, 是一个不可变序列,包含范围为 0 <= x < 256 的整数,bytes 是 bytearray 的不可变版本 - 它有其中不改变序列的方法和相同的索引、切片操作。
Python中bytes 字面值中只允许 ASCII 字符(无论源代码声明的编码为何)。 任何超出 127 的二进制值必须使用相应的转义序列形式加入 bytes 字面值。

bytearray和bytes不一样的地方在于,bytearray是可变的,它们的关系就相当于list与tuple

还是来看书上例子

1
2
3
4
5
6
7
8
9
10
11
12
>>> cafe = bytes('café', encoding='utf_8') ➊
>>> cafe
b'caf\xc3\xa9'
>>> cafe[0] ➋
99
>>> cafe[:1] ➌
b'c'
>>> cafe_arr = bytearray(cafe)
>>> cafe_arr ➍
bytearray(b'caf\xc3\xa9')
>>> cafe_arr[-1:] ➎
bytearray(b'\xa9')

❶ bytes 对象可以从 str 对象使用给定的编码构建。
❷ 各个元素是 range(256) 内的整数。
❸ bytes 对象的切片还是 bytes 对象,即使是只有一个字节的切片。
❹ bytearray 对象没有字面量句法,而是以 bytearray() 和字节序列字面量参数的形式
显示。
❺ bytearray 对象的切片还是 bytearray 对象。

my_bytes[0] 获取的是一个整数,而 my_bytes[:1] 返回的是一个长度为 1的 bytes 对象——这一点应该不会让人意外。s[0] == s[:1] 只对 str 这个序列类型成立。不过,str 类型的这个行为十分罕见。对其他各个序列类型来说,s[i] 返回一个元素,而 s[i:i+1] 返回一个相同类型的序列,里面是 s[i] 元素。

3.2 简单分析

上面的例子中,我们看到Python中字节用的是b'caf\xc3\xa9'来表示,通过一个前缀b来表示字节,也是为了让人更明白,cafe[0]得到的是99,我们打开ascii码表,或者用ord('c')知道了,99代表的就是’c’。从内部来看,这个字节在计算机中的真正存在方式是用01100011来表示,后面的af类似,而é在utf8编码中要用两个字节表示,而且这两个字节都大于127,所有只能使用十六进制转义。
那为什么要用十六进制呢,而不是二进制来表明字节?

由于字节(byte)在计算机内部出现的频率较高,如果可以使用一种简洁的方式将它的内在含义准确表达出来,将会给我们带来很多方便。选择十六进制,是因为8位二进制的数字可以方便的转换为2个十六进制的数字。一个字节能且只能由一对十六进制来表示,比如10110110可以表示为B6。如果使用4进制的话则需要使用4个数字来表示一个字节,不够简洁;使用8进制的话,最靠左的8进制数是由2位二进制数字来表示的,相比于使用16进制有些美中不足。

说到底,Python给我们返回来的字节形式还是“给人看的”,让我们以一种看字符的方式来看字节。

3.3 字节的两个好用方法来应对十六进制

fromhex(string) (与binascii.b2a_hex(string)类似):此 bytes 类方法返回一个解码给定字符串的 bytes 对象。 字符串必须由表示每个字节的两个十六进制数码构成,其中的 ASCII 空白符会被忽略
hex(h)(与binascii.a2b_hex(string)类似)):返回一个字符串对象,该对象包含实例中每个字节的两个十六进制数字。

1
2
3
4
5
6
7
8
9
10
>>> cafe = bytes('café', encoding='utf_8')
>>> cafe
b'caf\xc3\xa9'
>>> cafe.hex()
'636166c3a9'
>>>bytes.fromhex('63 61 66 c3 a9')
b'caf\xc3\xa9'
# 另一种写方法,用的不多,一些手册或者其他语言里的十六进制数据都是没有0x前缀,还要手动补,不靠谱
>>> bytes([0x63,0x61,0x66,0xc3,0xa9])
b'caf\xc3\xa9'

查看下bytes用法

Init signature: bytes(self, /, *args, **kwargs)
Docstring:
bytes(iterable_of_ints) -> bytes
bytes(string, encoding[, errors]) -> bytes
bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
bytes(int) -> bytes object of size given by the parameter initialized with null bytes
bytes() -> empty bytes object

好了,这下心里的结都理清楚了,特别是要给下位机发送一段十六进制的数字的组合,就可以这么转换了

4. 结构体与内存视图

struct 模块提供了一些函数,把打包的字节序列转换成不同类型字段组成的元组,还有一些函数用于执行反向转换,把元组转换成打包的bytes、bytearray 和 memoryview 对象。

虽然网上找的时候好多遇到了struct模块,但这里不多说,留下两个链接内存试图struct模块官方手册

5. 编码与解码

Python 自带了超过 100 种编解码器(codec, encoder/decoder),用于在文本和字节之间相互转换。每个编解码器都有一个名称,如 ‘utf_8’,而且经常有几个别名,如’utf8’、’utf-8’ 和 ‘U8’。这些名称可以传给open()、str.encode()、bytes.decode() 等函数的 encoding 参数。

终于到了编码与解码,其实万剑归一,那些科技大佬们在混沌的一片编码方式中劈开了一斧,创建了utf8,我们只要用它就行了。看下面一个例子。

1
2
3
4
5
>>>  k = '欢'.encode('utf8')
>>> k
b'\xe6\xac\xa2' # ➊
>>> print(bin(k[0])+' '+bin(k[1])+' '+bin(k[2]))
0b11100110 0b10101100 0b10100010 # ➋

❶ 汉字‘欢’通过utf8编码程三个字节
❷ 打印这三个字节的二进制

然后再来看Unicode字符代码与UTF-8编码的对应关系,发现与下面的第三条是对应的,所以编码也不是乱编的,你随便写一个字节再解码都是容易报错的,百分之九十九,除非你运气好。

1
2
3
4
5
6
7
8
 
0000 0000-0000 007F | 0xxxxxxx

0000 0080-0000 07FF | 110xxxxx 10xxxxxx

0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx

0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

6.尾声

在整理以上内容的过程中,又收获了新的知识,头脑里对这一块又清晰多了。字节字符编码解码无论在哪门语言中都是快让人头疼难啃的骨头,其实不在于Python中语法是怎样的,而是对概念的理解,人类对编码的越加完善,架设了这么一条人与计算机交流的的桥梁。科技的美妙在于此~