Python 字节串: 二进制数据的存储与处理
· 笔记
13 min
众所周知,在计算机中,无论是数字、文本,还是图片、音频,任何内容都是以二进制数据的形式存储和处理的。而之所以我们会看到不同类型的文件,是因为这些二进制数据被以不同的规则解释和编码,最终呈现为我们熟悉的形式。在这些文件中,纯文本内容通常只包含有限的字符,可以简单地通过 ASCII 或 UTF-8 等编码格式快速存储和读取。而图片、视频、压缩包等文件通常包含大量的数据与描述信息,必须靠特定的文件格式规范来正确解析和使用。
数字编码
正如数学上进制转换的自然性,数字格式的二进制表示也非常直观。简单理解就是直接将数字转换为二进制形式存储即可。具体细节层面,不同类型的整数、浮点数等数据类型会有不同的存储方式和字节数,但总体思路是一致的。
整数编码
无符号整数
无符号整数是最简单的整数类型,直接将数字转换为二进制形式存储即可。在具体的硬件实现中,通常会使用固定字节数来存储无符号整数,例如:
- 8bit (uint8): $ 0 \sim 2^8-1 = 255 $
- 32bit (uint32): $ 0 \sim 2^{32}-1 $
- 64bit (uint64): $ 0 \sim 2^{64}-1 $
对多字节整数,还需要考虑字节序(endianness)的问题。常见的字节序有两种:
大端序(Big-endian):高位字节存储在低地址处,低位字节存储在高地址处。
1x = 114514 2list(x.to_bytes(8, 'big')) 3# [0, 1, 191, 82]
大端序将最高位字节放在最前面,在直觉上更符合人类书写习惯,且方便快速识别数字的大小,因此在网络协议、文件格式等场景中被广泛采用。
小端序(Little-endian):低位字节存储在低地址处,高位字节存储在高地址处。
1x = 114514 2list(x.to_bytes(8, 'little')) 3# [82, 191, 1, 0]
小端序将最低位字节放在最前面,符合算术运算的自然顺序,天然适合向上扩展(例如从 uint32 扩展到 uint64 时,只需在高位补零,而不需要重新排列字节顺序)。因此,小端序更适合计算机运算,是现代 CPU 的主流选择。
有符号整数
在计算机发展历史中,原码、反码、补码等表示方法先后诞生,但最终补码成为了主流。补码利用模 $2^n$ 加法群的性质,即 $ -x = 2^n - x $,将负数映射到正数区间,从而简化了加减运算的实现。例如,8bit 补码表示的范围是 $ -128 \sim 127 $,其中:
- 正数的补码与其原码相同,例如 $ 5 $ 的补码是 $ 00000101 $。
- 负数的补码通过对其绝对值取反加一得到,例如 $ -5 $ 的补码是 $ 11111011 $(即 $ 256 - 5 $)。
类似地,32bit 和 64bit 补码的表示范围分别是 $ -2^{31} \sim 2^{31}-1 $ 和 $ -2^{63} \sim 2^{63}-1 $。
注意到,2147483647 这一数值在游戏数值、计数器等场景中经常出现,这其实正是 int32 整型的上限 $ 2^{31}-1 $.
在 Python 中,可以通过 int.to_bytes
方法将整数转换为字节串,指定 signed=True
参数表示有符号整数:
1x = -114514
2list(x.to_bytes(4, 'big', signed=True))
3# [255, 254, 64, 174]
浮点数编码
浮点数的二进制表示相对复杂,通常采用 IEEE 754 标准。该标准用二进制下的科学技术法表示浮点数,形式为
$$ x = (-1)^s \times ( 1 + m ) \times 2^{e - \text{bias}} $$
其中
- $ s $ 是符号位,0 表示正数,1 表示负数。
- $ m $ 是尾数(mantissa),表示有效数字部分,不包含整数位上的 1
- $ e $ 是指数(exponent),表示小数点的位置;通过偏移量(bias)进行编码以支持负指数。
根据字节数不同,常见的浮点数类型有单精度(32bit)和双精度(64bit)两种:
- 单精度浮点数使用 8 bits 指数位,偏移量 bias = 127;23 bits 尾数位,表示精度在 $ 2^{-23} \approx 1.19 \times 10^{-7} $ 量级。
- 双精度浮点数使用 11 bits 指数位,偏移量 bias = 1023;52 bits 尾数位,表示精度在 $ 2^{-52} \approx 2.22 \times 10^{-16} $ 量级。
精度的常见问题:由以上转换过程可以看出,十进制下的有限小数(如 0.1, 0.2)在二进制下可能是无限循环小数,因此无法精确表示,导致浮点数运算中出现精度误差。例如:
10.1 + 0.2 2# 0.30000000000000004
在 Python 中,可以通过 struct
模块将浮点数转换为字节串:
1import struct
2x = 3.14159
3struct.pack('>f', x) # 单精度,big-endian
4# b'@I\x0f\xd0'
5struct.pack('>d', x) # 双精度,big-endian
6# b'@\t!\xf9\xf0\x1b\x86n'
字符串编码
如前文所述,除数字自然可以转化为二进制数据外,其他内容都需要通过特定的编码规则进行转换。字符串是最常见的文本内容,由于字符集有限,其二进制表示相对简单,主要依赖于字符编码标准。
ASCII 编码
字符串的二进制表示相对简单,在计算机发展早期,英语系国家普遍采用 ASCII 编码,使用 7 bits 表示 128 个字符(包括 33 个控制字符和 95 个可打印字符)。
在 Python 中,可以通过 ascii
函数将字符串转换为 ASCII 编码的字节串,对 ASCII 可打印字符直接显示,对不可打印字符使用转义序列 \x
, \u
等表示:
1ascii('你好,SJTU!')
2# "'\\u4f60\\u597d\\uff0cSJTU!'"
Unicode 编码与 UTF-8
随着计算机的普及和国际化,ASCII 的局限性逐渐显现,尤其是无法表示非英语字符。为了解决这个问题,Unicode 标准应运而生,旨在为全球所有字符分配唯一的编码点(code point)。而 UTF-8 是 Unicode 的一种编码方式,将 Unicode 码点映射为字节序列,类似地映射方式还有 UTF-16、UTF-32 等,但 UTF-8 由于其高效性和向后兼容 ASCII 的特性,成为了目前最广泛使用的编码格式。
Unicode 为每个字符设定了唯一的码点,用 U+XXXX 的十六进制形式表示,例如在 iOS 输入法中输入苹果会看到苹果公司 Logo 这个图标,这其实是 Unicode 码点 U+F8FF,苹果在系统字体中将这个码点显示为自己的 Logo. 在我们查看它时,计算机先将二进制数据解码为 Unicode 码点,再通过字体文件将码点映射为具体的图形,这才有了我们看到的图标. 在 Python 中,可以通过 \u
或 \U
转义序列表示 Unicode 字符:
1'\u4F60\u597D' # '你好'
在 Python 中,可以通过 ord
函数获取字符的 Unicode 码点,通过 chr
函数将码点转换为字符:
1hex(ord('S')), chr(83)
2# (0x53, 'S')
3hex(ord('')), chr(0xF8FF)
4# (0xF8FF, '\uf8ff')
Python 的字节串
在 Python 中,bytes
类型用于表示字节串,即一系列字节的有序集合。
Python 中还有一个类似的类型
bytearray
,它是bytes
的可变版本,类似于list
和tuple
的关系,二者的内容并没有任何区别。
字节串的本质是一串二进制数据,通过 list(bytes_obj)
就可以看出,每个字节都是一个 0-255 之间的整数,即其数据的十进制表示。为了表示方便,字节串类型还有一个重要属性——字面量。字面量是以 b'...'
或 b"..."
形式表示的字节串常量,使用转义序列 \xhh
表示十六进制的字节值,若该字节为 ASCII 可打印字符,则直接显示该字符
1bytes('你好,SJTU', 'utf8')
2# b'\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8cSJTU'
Python 提供了多种方式创建字节串:
通过字面量直接表示:正如字面量的表示规则,这种方式只能将 ASCII 可打印字符直接写入,其他字节需要使用转义序列表示。
1b'Hello, SJTU!' 2# b'Hello, SJTU!'
bytes(str, encoding)
:将字符串按照指定编码转换为字节串。1bytes('你好,SJTU', 'utf8') 2# b'\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8cSJTU'
bytes(iterable_of_ints)
:将整数序列转换为字节串,整数必须在 0-255 范围内,可以是任意进制.1bytes([228, 189, 160, 229, 165, 189, 239, 188, 140, 83, 74, 84, 85]) 2# b'\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8cSJTU'
str.encode(encoding)
:将字符串按照指定编码转换为字节串,等价于bytes(str, encoding)
。1'你好,SJTU'.encode('utf8') 2# b'\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8cSJTU'