在 Python 3 中,str 是处理文本的主力,但网络数据包、图片文件、二进制协议、加密解密等场景,必须使用 bytes 或 bytearray。核心关系是:str 是 Unicode 字符序列,bytes 和 bytearray 是 0-255 的整数序列,两者通过 encode() 和 decode() 相互转换。
一、字符、编码与字节
计算机只能存储 bit,8 个 bit 组成一个 byte,一个字节可表示 0-255。字符要存储和传输,必须先编码成字节序列。同一个字符在不同编码下字节数不同,例如“中”在 UTF-8 下占 3 字节,在 GBK 下占 2 字节,在 ASCII 下无法编码。
- char = '中'
- print(len(char.encode('utf-8')))
- print(len(char.encode('gbk')))
复制代码
str 的元素是长度为 1 的 str,bytes 的元素是 int。下面代码中 len(text) 是字符数 8,len(data) 是字节数 12,data[0] 是 80,而不是字符 'P'。
- text = 'Python编程'
- data = text.encode('utf-8')
- print(type(text), len(text), text[0])
- print(type(data), len(data), data[0])
复制代码
二、bytes 创建与操作
bytes 是不可变字节序列。常见创建方式包括 b 前缀字面量、bytes() 整数序列、指定长度生成全零字节,以及从 str 调用 encode()。
- data1 = b'hello'
- data2 = bytes([72, 101, 108, 108, 111])
- data3 = bytes(10)
- data4 = 'Python编程'.encode('utf-8')
- print(data1)
- print(data2)
- print(len(data3))
- print(data4)
复制代码
bytes 索引访问返回整数,切片返回 bytes;成员检查可以用整数,也可以用单字节 bytes。常用方法包括 find、rfind、index、count、startswith、endswith、split、partition、replace、strip、isalpha、isdigit、join 等。
- data = b'hello world hello python'
- print(data[0])
- print(data[:5])
- print(data.find(b'hello'))
- print(data.rfind(b'hello'))
- print(data.count(b'hello'))
- print(data.split())
- print(data.replace(b'hello', b'hi', 1))
- print(b' '.join([b'hello', b'world']))
复制代码
因为 bytes 不可变,不能执行 data[0] = 106,否则会抛 TypeError。
三、bytearray 可变字节序列
bytearray 是 bytes 的可变版本,支持原地修改,适合网络缓冲区累积、图像数据处理等频繁修改字节的场景。它的元素仍然是 0-255 整数。
- ba = bytearray(b'hello world')
- ba[0] = ord('H')
- print(ba)
- ba[6:11] = b'python'
- print(ba)
- ba.append(33)
- ba.extend(b' world')
- ba.insert(5, ord(','))
- print(ba)
- last = ba.pop()
- print(last)
- ba.reverse()
- print(ba)
复制代码
bytearray 提供类似 list 的 append、extend、insert、pop、remove、reverse、clear,也有 copy() 方法。bytes(ba) 可以得到不可变拷贝。查找、替换、大小写等方法和 bytes 类似。
四、encode/decode 与错误处理
encode 从 str 到 bytes,decode 从 bytes 到 str。用错编码会乱码或抛 UnicodeDecodeError。errors 参数控制异常字符处理:strict 默认抛异常;ignore 忽略;replace 替换为 ? 或 U+FFFD;xmlcharrefreplace、backslashreplace、namereplace 用于 ASCII 编码时替换不可编码字符。
- text = 'Python编程'
- utf8_data = text.encode('utf-8')
- print(utf8_data.decode('utf-8'))
- gbk_data = text.encode('gbk')
- print(gbk_data.decode('gbk'))
- bad = b'hello' + bytes([255]) + b'world'
- print(bad.decode('utf-8', errors='ignore'))
- print(bad.decode('utf-8', errors='replace'))
复制代码
处理未知编码时,可以按常见编码依次尝试解码。下面函数尝试 utf-8、gbk、gb2312、iso-8859-1、windows-1252,并用替换字符比例做简单判断。
- def safe_decode(data):
- encodings = ['utf-8', 'gbk', 'gb2312', 'iso-8859-1', 'windows-1252']
- for enc in encodings:
- try:
- decoded = data.decode(enc)
- if decoded.count('�') / max(len(decoded), 1) < 0.1:
- return enc, decoded
- except (UnicodeDecodeError, UnicodeError):
- continue
- return 'utf-8', data.decode('utf-8', errors='replace')
复制代码
BOM 是字节顺序标记。UTF-8 BOM 为 EF BB BF,utf-8-sig 编码会写入或跳过 BOM;UTF-16 BOM 为 FE FF 或 FF FE,用于标识大端或小端。处理带 BOM 文件时,可以检测开头并跳过,更简单的方式是使用 encoding='utf-8-sig'。
五、文件 I/O 与二进制数据
文本模式读写自动编码解码,返回 str;二进制模式读写返回 bytes,适合图片、音频、视频、压缩包等非文本文件。大文件复制应分块读取,避免一次性占满内存。
- def copy_file_binary(src, dst):
- with open(src, 'rb') as f_in:
- with open(dst, 'wb') as f_out:
- while True:
- chunk = f_in.read(8192)
- if not chunk:
- break
- f_out.write(chunk)
复制代码
文件类型可以通过文件头魔数判断。例如 PNG、JPEG、GIF、PDF、ZIP、DOCX/XLSX 都有固定开头的字节序列。读取前几个字节,再用 startswith 比较,即可做简单类型检测。
六、网络编程与 struct
socket 发送和接收的都是 bytes。构建自定义二进制协议时,可以用 struct.pack 打包整数。例如协议格式为:魔数 2 字节、消息 ID 4 字节、消息类型 2 字节、载荷长度 2 字节、载荷 N 字节。
- import struct
- magic = bytes([0xAB, 0xCD])
- payload_bytes = 'hello'.encode('utf-8')
- header = struct.pack('!IHH', 1001, 2, len(payload_bytes))
- packet = magic + header + payload_bytes
- print(packet)
复制代码
这里的 ! 表示网络字节序(大端),I 是 4 字节无符号整数,H 是 2 字节无符号整数。接收端解析时,要按同样格式 unpack,并校验魔数和载荷长度,否则容易读到错误偏移或截断数据。
七、常见错误与选择建议
经典错误是把 str 直接当 bytes 用,例如 socket.send 传 str,或对 str 调用 decode,会报 TypeError: a bytes-like object is required。排查时先确认数据来源是文本还是二进制:文本转字节用 encode,字节转文本用 decode;检查编码是否匹配;未知编码先尝试 utf-8、gbk 等,再用 errors='replace' 兜底。bytes 与十六进制字符串互转可借助 bytes.fromhex() 和 bytes.hex(),便于打印和比对二进制数据。
性能与适用场景方面,bytes 不可变,传递和哈希更安全,适合只读数据;bytearray 可变,频繁修改时能避免反复创建新对象,适合缓冲区。str 与 bytes 不要混用,转换必须显式指定编码,才能在文件、网络和二进制协议处理中稳定落地。 |