str 与 bytes:文本、二进制与编码
核心原则
磁盘和网络只认识 bytes,应用层代码操作 str。文件打开模式决定 Python 是否自动在两者之间转换:文本模式自动转换,二进制模式由开发者手动处理。
本质区别
这是 Python 3 最核心的设计变更之一:严格区分文本(str)和二进制(bytes)。
| 类型 | str | bytes |
|---|---|---|
| 本质 | Unicode 字符序列 | 原始字节序列(0–255 的整数) |
| 存储 | 抽象字符(不固定字节数) | 具体字节(一个元素 = 1 字节) |
| 字面量前缀 | 'text' 或 u'text' | b'text' |
| 适用场景 | 人类可读文本、代码、JSON | 图片、视频、网络数据包、加密数据 |
| 长度计算 | len('中') → 1(字符数) | len('中'.encode('utf-8')) → 3(字节数) |
text = 'Hello 世界' # str:默认字符串类型
binary = b'Hello World' # bytes:只接受 ASCII 字符或原始字节值
print(type(text)) # <class 'str'>
print(type(binary)) # <class 'bytes'>
编码与解码
str 和 bytes 之间必须通过明确的编码规则转换,Python 3 不做隐式转换。
str (Unicode) ──encode()──> bytes (二进制)
<──decode()──
str → bytes:编码
text = 'Hello 世界'
# UTF-8:最通用,中文占 3 字节,英文占 1 字节
utf8_bytes = text.encode('utf-8')
print(utf8_bytes) # b'Hello \xe4\xb8\x96\xe7\x95\x8c'
# GBK:中文 Windows 默认编码,中文占 2 字节
gbk_bytes = text.encode('gbk')
print(gbk_bytes) # b'Hello \xc8\xc0\xbd\xe7'
# ASCII 无法编码中文
try:
text.encode('ascii')
except UnicodeEncodeError:
print('ASCII 不支持中文')
bytes → str:解码
raw = b'Hello \xe4\xb8\x96\xe7\x95\x8c'
# 正确解码
text = raw.decode('utf-8')
print(text) # Hello 世界
# 解码规则不匹配,产生乱码或报错
try:
wrong = raw.decode('gbk')
print(wrong) # 乱码:Hello 涓栫晫
except UnicodeDecodeError:
print('解码失败')
文件操作中的文本模式与二进制模式
文本模式:自动编解码
# 写入时,Python 自动将 str 编码为字节后写入磁盘
with open('utf8.txt', 'w', encoding='utf-8') as f:
f.write('Hello 世界') # 传入 str,内部转为 b'Hello \xe4\xb8\x96\xe7\x95\x8c'
# 读取时,Python 自动将磁盘字节解码为 str
with open('utf8.txt', 'r', encoding='utf-8') as f:
content = f.read()
print(type(content)) # <class 'str'>
二进制模式:直接操作 bytes
# 写入必须传入 bytes,原样写入磁盘
with open('raw.bin', 'wb') as f:
f.write(b'\x00\x01\x02') # 正确:写入原始字节
# f.write('Hello') # 错误:TypeError
f.write('Hello'.encode('utf-8')) # 手动编码后写入
# 读取返回 bytes,不做任何解码
with open('raw.bin', 'rb') as f:
data = f.read()
print(type(data)) # <class 'bytes'>
print(data[0]) # 0(整数)
常见陷阱
陷阱 1:混用模式导致 TypeError
# 文本模式写入 bytes
with open('test.txt', 'w') as f:
f.write(b'Hello') # TypeError: write() argument must be str, not bytes
# 二进制模式写入 str
with open('test.bin', 'wb') as f:
f.write('Hello') # TypeError: a bytes-like object is required, not 'str'
规则:
-
传入
b'...'→ 用'rb'/'wb'模式 -
传入
'...'→ 用'r'/'w'模式并指定encoding
陷阱 2:编码不一致导致乱码
# 写入用 UTF-8
with open('config.txt', 'w', encoding='utf-8') as f:
f.write('配置信息')
# 读取用 GBK
with open('config.txt', 'r', encoding='gbk') as f:
print(f.read()) # 乱码:閰嶇疆淇℃伅
始终用相同的编码写入和读取同一文件。
陷阱 3:BOM 头(字节顺序标记)
Windows 记事本保存 UTF-8 文件时,可能在文件开头写入 BOM(\xef\xbb\xbf),导致读取时出现不可见字符。
# 检测并手动去除 BOM
with open('with_bom.txt', 'r', encoding='utf-8') as f:
content = f.read()
if content.startswith('\ufeff'):
content = content[1:]
# 更简洁:使用 utf-8-sig 自动处理 BOM
with open('with_bom.txt', 'r', encoding='utf-8-sig') as f:
content = f.read() # BOM 已自动剔除
实战场景
场景 1:读取图片并计算哈希
import hashlib
# 图片是二进制数据,必须用 'rb'
with open('photo.jpg', 'rb') as f:
img_bytes = f.read()
md5_hash = hashlib.md5(img_bytes).hexdigest()
print(md5_hash)
场景 2:处理 HTTP 响应(requests 库)
import requests
response = requests.get('https://api.example.com/data')
raw_bytes = response.content # bytes(原始未解码)
text_str = response.text # str(已按响应头编码解码)
# 自动解码失败时手动处理
try:
text = response.content.decode('utf-8')
except UnicodeDecodeError:
text = response.content.decode('gbk', errors='ignore')
场景 3:自定义二进制协议(文件魔数)
# 读取:前 4 字节是魔数,其余是 UTF-8 文本
with open('custom.bin', 'rb') as f:
magic = f.read(4)
if magic != b'MYFM':
raise ValueError('非法文件格式')
text = f.read().decode('utf-8')
# 写入同样格式
with open('custom.bin', 'wb') as f:
f.write(b'MYFM')
f.write('实际内容'.encode('utf-8'))
进阶:bytearray 可变字节序列
bytes 是不可变的(类似 tuple)。如果需要原地修改二进制数据,使用 bytearray:
ba = bytearray(b'Hello')
ba[0] = ord('J') # 修改单个字节,变为 b'Jello'
# 常用于大文件缓冲区的原地修改
with open('data.bin', 'rb') as f:
chunk = bytearray(f.read(1024))
chunk[0] = 0xFF
总结速查
| 场景 | 操作 | 结果类型 |
|---|---|---|
| 文本文件读写 | 'r'/'w' + encoding | 自动处理 str 与磁盘字节的转换 |
| 图片、视频、加密数据 | 'rb'/'wb' | 手动处理 bytes |
| 网络接收数据 | .decode('utf-8') | bytes → str |
| 网络发送数据 | .encode('utf-8') | str → bytes |
| 编码未知时容错 | errors='ignore' 或 'replace' | 跳过或替换无法解码的字节 |