3. 二进制文件操作:图片、音频文件的读写,struct模块打包与解包
说实话,很多初学者一听到「二进制文件」就头大。我当年也是。
总觉得文本文件多好理解,打开就是字符。二进制文件呢?一堆乱码,根本看不懂。
但你要知道,图片、音频、视频、可执行程序……这些才是计算机世界的常态。文本文件反而是特例。
这一章,咱们就把二进制文件这块硬骨头啃下来。
3.1 文本文件 vs 二进制文件
先搞清楚一个基本概念:文本文件也是二进制文件的一种。
区别在哪?文本文件存的是「字符编码」,比如你用记事本打开能看懂。二进制文件存的是「原始字节」,比如图片的像素数据、音频的采样值。
| 对比项 | 文本文件 | 二进制文件 |
|---|---|---|
| 存储内容 | 字符编码(如UTF-8) | 原始字节数据 |
| 打开方式 | 文本编辑器可读 | 需要专用程序解析 |
| Python模式 | 'r' 或 'w' |
'rb' 或 'wb' |
| 典型文件 | .txt, .py, .html | .jpg, .mp3, .exe |
嗯,这里要注意:用 'rb' 模式打开文件,读出来的是 bytes 对象。用 'wb' 写入,也得传 bytes 进去。
3.2 图片文件的读写
先来个最简单的——复制一张图片。
# 读取图片的二进制数据
with open('input.jpg', 'rb') as f:
data = f.read()
# 写入到新文件
with open('output.jpg', 'wb') as f:
f.write(data)
print(f'图片大小:{len(data)} 字节')
就这么简单?对,就这么简单。你想想看,图片在磁盘上就是一堆字节。读出来,写进去,复制完成。
但实际项目中,我们往往需要处理图片的元数据。比如图片的宽高、色彩模式、压缩格式。
我曾经接手过一个项目,需要批量验证用户上传的图片是不是真正的JPEG文件。有人改后缀名来骗系统。
怎么判断?看文件头。
def is_jpeg(filepath):
"""检查文件是否为JPEG格式"""
with open(filepath, 'rb') as f:
header = f.read(4)
# JPEG文件头固定为 FF D8 FF E0 或 FF D8 FF E1
return header[:2] == b'\xff\xd8' and header[2:4] in (b'\xff\xe0', b'\xff\xe1')
# 测试
print(is_jpeg('photo.jpg')) # True
print(is_jpeg('fake.txt')) # False
说白了,每种文件格式都有固定的「魔数」(Magic Number)。PNG是 89 50 4E 47,GIF是 47 49 46 38。记住这些特征,你就能做很多有趣的事情。
3.3 音频文件的读写
音频文件稍微复杂一点。以WAV格式为例,它有一个文件头,后面跟着采样数据。
WAV文件头包含:采样率、位深度、声道数等信息。我习惯用Python的 wave 模块来处理,它帮我们封装好了这些细节。
import wave
# 读取WAV文件
with wave.open('audio.wav', 'rb') as wav:
print(f'声道数:{wav.getnchannels()}')
print(f'采样宽度:{wav.getsampwidth()} 字节')
print(f'采样率:{wav.getframerate()} Hz')
print(f'总帧数:{wav.getnframes()}')
# 读取所有音频数据
frames = wav.readframes(wav.getnframes())
print(f'音频数据大小:{len(frames)} 字节')
你可能会问:读出来的 frames 是什么?就是一堆二进制字节。每个采样点按位深度编码成字节。
比如16位采样,每个采样点占2个字节。如果是双声道,左右声道交替排列。
我建议你动手试一下:把一段音频的采样值打印出来,看看波形长什么样。
import struct
# 假设是16位单声道WAV
sample_count = len(frames) // 2
samples = struct.unpack('<' + 'h' * sample_count, frames)
# 打印前10个采样值
print(samples[:10])
看到没?音频数据本质上就是一组整数。正数表示波峰,负数表示波谷。这就是数字音频的底层真相。
3.4 struct模块:打包与解包
刚才我们用到了 struct.unpack。这个模块是处理二进制数据的瑞士军刀。
它的核心功能就两个:
- pack:把Python值打包成字节
- unpack:把字节解析成Python值
格式字符串决定了怎么打包/解包。常用的格式符:
| 格式符 | C类型 | Python类型 | 字节数 |
|---|---|---|---|
b |
signed char | int | 1 |
B |
unsigned char | int | 1 |
h |
short | int | 2 |
H |
unsigned short | int | 2 |
i |
int | int | 4 |
f |
float | float | 4 |
d |
double | float | 8 |
格式字符串前面还可以加字节序前缀:< 小端,> 大端,! 网络序。
举个例子,假设我们要解析一个自定义的二进制协议:
import struct
# 模拟收到的二进制数据
# 协议:4字节整数ID + 2字节短整数年龄 + 4字节浮点数字段
raw_data = b'\x01\x00\x00\x00\x1e\x00\x00\x00\x40\x40'
# 解包
id_val, age, score = struct.unpack('<ihf', raw_data)
print(f'ID: {id_val}, 年龄: {age}, 分数: {score}')
# 输出:ID: 1, 年龄: 30, 分数: 3.0
反过来,打包也很简单:
# 打包成二进制
packed = struct.pack('<ihf', 2, 25, 95.5)
print(packed.hex())
# 输出:02000000190000000000bf42
= 使用标准大小。
3.5 实战:自定义二进制文件格式
让我给你展示一个完整的例子。假设我们要保存一组学生的成绩数据。
用文本文件当然可以,但二进制文件更紧凑、解析更快。
import struct
# 定义格式:学号(4字节int) + 姓名(20字节bytes) + 成绩(4字节float)
RECORD_FORMAT = '<i20sf'
RECORD_SIZE = struct.calcsize(RECORD_FORMAT)
def save_records(filename, records):
"""保存学生记录到二进制文件"""
with open(filename, 'wb') as f:
for sid, name, score in records:
# 姓名补足20字节
name_bytes = name.encode('utf-8').ljust(20, b'\x00')
packed = struct.pack(RECORD_FORMAT, sid, name_bytes, score)
f.write(packed)
def load_records(filename):
"""从二进制文件加载学生记录"""
records = []
with open(filename, 'rb') as f:
while True:
chunk = f.read(RECORD_SIZE)
if len(chunk) < RECORD_SIZE:
break
sid, name_bytes, score = struct.unpack(RECORD_FORMAT, chunk)
# 去掉末尾的空字节
name = name_bytes.rstrip(b'\x00').decode('utf-8')
records.append((sid, name, score))
return records
# 测试
students = [
(1001, '张三', 88.5),
(1002, '李四', 92.0),
(1003, '王五', 76.5),
]
save_records('students.dat', students)
loaded = load_records('students.dat')
print(loaded)
struct.calcsize 计算记录长度,比手动算字节数靠谱得多。我曾经因为算错字节数,导致整个数据文件解析错位,排查了半天。
3.6 知识体系总览
下面这张图,帮你理清本章的核心脉络:
3.7 避坑指南
最后,分享几个我踩过的坑:
- 字节序搞反了:我曾经解析一个网络协议,忘了用
!网络序,结果解析出来的数值完全不对。排查了两小时才发现。 - 忘记处理EOF:读取二进制文件时,最后一块可能不足一个记录长度。一定要检查
len(chunk)。 - 字符串编码问题:二进制文件里的字符串,可能是ASCII、UTF-8、甚至GBK。读出来之前先确认编码。
- 大文件不要一次读完:几百MB的图片或音频,用
f.read()会撑爆内存。分块读取才是正道。
核心要点:二进制文件操作没那么神秘。记住三点——用 'rb'/'wb' 模式打开,用 struct 解析结构化数据,注意字节序和对齐。掌握了这些,你就能和任何二进制格式打交道。
可可精品资料库