在Python开发中,处理字符串(str)时一切看起来都很自然,但当你需要读取二进制文件、解析网络数据包或进行加密计算时,就会遇到bytes和bytearray。不少初学者混淆它们与str的关系,甚至遇到类似“a bytes-like object is required”的错误。本文从底层原理出发,梳理bytes和bytearray的区别、创建与操作方式、编码解码规则,并给出文件I/O、网络编程、struct打包等典型场景的代码实践。
一、为什么需要bytes?
计算机只能存储二进制数据(0和1),8个bit组成一个byte,可以表示0~255的整数。人类使用的文字、符号需要经过编码转换为字节序列才能存储和传输。Python中str是Unicode字符序列,而bytes是0~255的整数序列。两者通过encode()和decode()相互转换。
同一个字符在不同编码下字节长度不同:- char = '中'
- print(char.encode('utf-8')) # b'\xe4\xb8\xad' (3字节)
- print(char.encode('gbk')) # b'\xd6\xd0' (2字节)
- print(char.encode('ascii')) # 报错UnicodeEncodeError
复制代码
str的元素是长度为1的字符串,而bytes的元素是整数:- text = 'Python编程'
- data = text.encode('utf-8')
- print(data[0]) # 80 (整数)
- print(text[0]) # 'P' (字符串)
复制代码
二、bytes——不可变的字节序列
2.1 创建bytes的四种方式
1. 使用b前缀字面量(仅限ASCII字符或转义序列)
2. bytes()构造函数从整数序列创建
3. bytes()构造函数指定长度(全零)
4. 通过str.encode()方法
- # 方式一:b前缀
- b'hello'
- b'\xe4\xbd\xa0\xe5\xa5\xbd' # '你好'的UTF-8转义
- # 方式二:整数列表
- bytes([72,101,108,108,111]) # b'Hello'
- # 方式三:指定长度
- bytes(5) # b'\x00\x00\x00\x00\x00'
- # 方式四:encode
- 'Python编程'.encode('utf-8') # b'Python\xe7\xbc\x96\xe7\xa8\x8b'
复制代码
2.2 基本操作(索引、切片、成员检查)
- data = b'hello world'
- print(data[0]) # 104 (整数)
- print(data[:5]) # b'hello'
- print(104 in data) # True
- print(b'hello' in data) # True
- print(list(b'ABC')) # [65,66,67]
复制代码
2.3 常用方法
bytes支持类似str的方法,但操作对象是字节:- data = b'hello world hello python'
- print(data.find(b'hello')) # 0
- print(data.count(b'hello')) # 2
- print(data.replace(b'hello',b'hi')) # b'hi world hi python'
- print(b' hello '.strip()) # b'hello'
- print(b' '.join([b'a',b'b'])) # b'a b'
复制代码
三、bytearray——可变的字节序列
bytearray与bytes类似,但可以原地修改,适合需要频繁修改二进制数据的场景(如网络缓冲、图像处理)。
- ba = bytearray(b'hello')
- ba[0] = 106 # 改为'j' (ASCII 106)
- print(ba) # bytearray(b'jello')
- # bytes不可变,赋值会报错
- # b'hello'[0] = 106 # TypeError
复制代码
3.1 创建与修改操作
- ba = bytearray(b'hello world')
- ba[0] = ord('H')
- ba[6:11] = b'python' # 切片赋值
- ba.append(33) # 追加感叹号
- ba.extend(b' world') # 扩展
- ba.insert(5, ord(',')) # 插入
- ba.pop() # 弹出末尾
- ba.remove(ord('a')) # 移除第一次出现的字节值
- ba.reverse() # 反转
- ba.clear() # 清空
复制代码
3.2 特有方法
bytearray有copy()方法,返回独立拷贝:- ba = bytearray(b'Hello')
- copied = ba.copy()
- ba[0] = 106
- print(ba) # bytearray(b'jello')
- print(copied) # bytearray(b'Hello')
复制代码
四、编码与解码——str与bytes的桥梁
4.1 encode()——从str到bytes
- text = 'Python编程'
- print(text.encode('utf-8'))
- print(text.encode('gbk'))
- print(text.encode('ascii', errors='replace')) # b'Python????'
复制代码 errors参数支持strict(默认)、ignore、replace、xmlcharrefreplace、backslashreplace、namereplace。
4.2 decode()——从bytes到str
- data = b'Python\xe7\xbc\x96\xe7\xa8\x8b'
- print(data.decode('utf-8')) # Python编程
- # 错误编码导致乱码或异常
- corrupted = b'hello\xffworld'
- print(corrupted.decode('utf-8', errors='replace')) # hello?world
复制代码
实际中可编写安全解码函数,尝试多种编码:- 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('\ufffd') / max(len(decoded),1) < 0.1:
- return enc, decoded
- except:
- continue
- return 'utf-8', data.decode('utf-8', errors='replace')
复制代码
4.3 BOM(字节顺序标记)
BOM用于标识编码和字节序,UTF-8 BOM为EF BB BF,UTF-16 LE BOM为FF FE,UTF-16 BE BOM为FE FF。处理文件时可检测并移除BOM:- raw = b'\xef\xbb\xbfhello'
- if raw.startswith(b'\xef\xbb\xbf'):
- text = raw[3:].decode('utf-8')
复制代码 Python的utf-8-sig编码可自动处理BOM:- with open('test.txt','w',encoding='utf-8-sig') as f:
- f.write('带BOM的文字')
复制代码
五、实用场景
5.1 文件I/O——二进制模式
处理非文本文件(图片、音频等)必须使用二进制模式:- with open('image.jpg','rb') as f:
- data = f.read()
- # 分块拷贝大文件
- def copy_binary(src, dst):
- with open(src,'rb') as fin, open(dst,'wb') as fout:
- while True:
- chunk = fin.read(8192)
- if not chunk: break
- fout.write(chunk)
复制代码 通过文件头魔数判断文件类型:- def detect_type(filepath):
- magic = {
- b'\x89PNG\r\n\x1a\n':'PNG',
- b'\xff\xd8\xff':'JPEG',
- b'%PDF':'PDF',
- b'PK\x03\x04':'ZIP',
- }
- with open(filepath,'rb') as f:
- head = f.read(8)
- for m,t in magic.items():
- if head.startswith(m):
- return t
- return '未知'
复制代码
5.2 网络编程
socket收发数据均为bytes:- import socket
- sock = socket.socket()
- sock.connect(('httpbin.org',80))
- request = b'GET /get HTTP/1.1\r\nHost: httpbin.org\r\n\r\n'
- sock.sendall(request)
- response = b''
- while True:
- chunk = sock.recv(4096)
- if not chunk: break
- response += chunk
- sock.close()
复制代码
5.3 使用struct模块打包二进制数据
处理自定义二进制协议:- import struct
- # 打包:魔数(2B) + 消息ID(4B) + 类型(2B) + 长度(2B) + 载荷
- magic = b'\xAB\xCD'
- msg_id, msg_type, payload = 1, 0, 'Hello'
- payload_bytes = payload.encode('utf-8')
- header = struct.pack('!IHH', msg_id, msg_type, len(payload_bytes))
- packet = magic + header + payload_bytes
复制代码
六、常见错误与调试
常见错误:TypeError: a bytes-like object is required, not 'str'。
通常发生在需要bytes的地方传入了str,比如socket.sendall()传入字符串。解决方法是先encode()。
bytes与十六进制字符串互转:- # bytes -> hex string
- print(b'hello'.hex()) # '68656c6c6f'
- # hex string -> bytes
- print(bytes.fromhex('68656c6c6f')) # b'hello'
复制代码
七、总结
bytes是不可变的字节序列,bytearray是可变的。str与bytes通过encode/decode转换。处理二进制文件、网络通信、协议解析时,必须使用bytes或bytearray。掌握它们的创建、操作和编码解码细节,能避免开发中常见的类型错误和乱码问题。 |