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
注意字节对齐:struct默认会按C语言的对齐规则填充字节。如果你要精确控制,可以在格式字符串前加 = 使用标准大小。

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 知识体系总览

下面这张图,帮你理清本章的核心脉络:

二进制文件操作知识体系 二进制文件操作 图片文件读写 音频文件读写 struct模块 文件头魔数检测 JPEG/PNG格式 二进制复制 WAV文件头解析 采样数据读取 声道/位深度 pack打包 unpack解包 格式字符串 核心:一切文件都是字节,学会用struct解析

3.7 避坑指南

最后,分享几个我踩过的坑:

  • 字节序搞反了:我曾经解析一个网络协议,忘了用 ! 网络序,结果解析出来的数值完全不对。排查了两小时才发现。
  • 忘记处理EOF:读取二进制文件时,最后一块可能不足一个记录长度。一定要检查 len(chunk)。
  • 字符串编码问题:二进制文件里的字符串,可能是ASCII、UTF-8、甚至GBK。读出来之前先确认编码。
  • 大文件不要一次读完:几百MB的图片或音频,用 f.read() 会撑爆内存。分块读取才是正道。

核心要点:二进制文件操作没那么神秘。记住三点——用 'rb'/'wb' 模式打开,用 struct 解析结构化数据,注意字节序和对齐。掌握了这些,你就能和任何二进制格式打交道。

可可精品资料库