二进制文件操作:不只是文本的世界

说实话,很多初学者学文件操作时,都默认文件里存的是文字。但现实世界哪有这么简单?图片、音频、视频、可执行程序……这些都不是纯文本。我刚开始做项目时,就吃过这个亏——用文本模式去读一张图片,结果全是乱码,程序直接崩溃。

二进制模式,说白了就是让Python把文件当成「一串字节」来处理。没有编码转换,没有换行符处理,你给什么就读什么。这才是文件操作的底层真相。

二进制模式的三种打开方式

Python里用 'b' 来表示二进制模式。配合读写操作,有三种常用组合:

模式 含义 文件指针位置 如果文件不存在
rb 二进制读取 开头 报错
wb 二进制写入 开头(覆盖) 新建
ab 二进制追加 末尾 新建

嗯,这里要注意:wb 模式会直接清空原文件。我曾经在调试时手滑写错了变量名,把一个重要的配置文件给覆盖了……从那以后,我写二进制写入前都会先备份。

实战:复制一张图片

咱们直接上手。复制图片,就是二进制文件操作最典型的应用:

with open('input.jpg', 'rb') as src:
    data = src.read()

with open('output.jpg', 'wb') as dst:
    dst.write(data)

print('图片复制完成!')

你看,就这么几行。图片、音频、视频,原理都一样。我有个朋友做爬虫,每天要下载上千张商品图,用的就是这套逻辑。

但有个坑——如果图片很大(比如几百MB),一次性 read() 会吃掉大量内存。我建议用分块读取:

with open('input.jpg', 'rb') as src, open('output.jpg', 'wb') as dst:
    while True:
        chunk = src.read(4096)  # 每次读4KB
        if not chunk:
            break
        dst.write(chunk)

为什么是4096?其实这是个经验值。4KB是大多数文件系统的簇大小,读写效率比较高。你想想看,如果每次只读1个字节,那得调用多少次系统函数?

处理音频文件:小心头部信息

音频文件比图片更「娇气」。比如WAV文件,开头有一段44字节的文件头,里面记录了采样率、位深、声道数等信息。如果你直接修改后面的音频数据,但忘了更新文件头……嗯,播放器会直接罢工。

我做过一个音频剪辑工具,踩过这个坑。当时我直接修改了PCM数据,结果播放出来全是噪音。排查了半天,才发现是文件头里的数据长度字段没更新。

import struct

# 读取WAV文件头(前44字节)
with open('audio.wav', 'rb') as f:
    header = f.read(44)

# 解析文件头
chunk_id = header[0:4]          # b'RIFF'
file_size = struct.unpack('<I', header[4:8])[0]
wave_id = header[8:12]          # b'WAVE'
fmt_id = header[12:16]          # b'fmt '
fmt_size = struct.unpack('<I', header[16:20])[0]
audio_format = struct.unpack('<H', header[20:22])[0]
num_channels = struct.unpack('<H', header[22:24])[0]
sample_rate = struct.unpack('<I', header[24:28])[0]
byte_rate = struct.unpack('<I', header[28:32])[0]
block_align = struct.unpack('<H', header[32:34])[0]
bits_per_sample = struct.unpack('<H', header[34:36])[0]

print(f'采样率: {sample_rate} Hz')
print(f'声道数: {num_channels}')
print(f'位深: {bits_per_sample} bit')

核心要点:二进制文件操作的关键,就是你要知道文件的「结构」。图片有图片的格式规范,音频有音频的协议标准。不了解结构,你就是在瞎改。

struct模块:打包与解包的利器

刚才的代码里用到了 struct 模块。它的作用很简单:把Python的数据类型(整数、浮点数、字符串)转换成固定长度的字节串,或者反过来。

我个人习惯用 struct.pack() 来打包数据,用 struct.unpack() 来解包。格式字符串是它的灵魂:

格式符 含义 字节数
i 有符号整数 4
I 无符号整数 4
h 有符号短整数 2
H 无符号短整数 2
f 浮点数 4
d 双精度浮点数 8
s 字节串 指定长度

还有字节序的修饰符:< 表示小端序,> 表示大端序。为什么要有这个?因为不同CPU架构存储多字节数据的方式不一样。Intel的CPU用小端序,网络协议通常用大端序。我在做跨平台工具时,就因为这个字节序问题折腾了一整天。

import struct

# 打包:把Python数据变成字节串
packed = struct.pack('<I 2H f', 100, 1, 2, 3.14)
print(packed)  # 输出 b'd\x00\x00\x00\x01\x00\x02\x00\xc3\xf5H@'

# 解包:把字节串变回Python数据
unpacked = struct.unpack('<I 2H f', packed)
print(unpacked)  # 输出 (100, 1, 2, 3.140000104904175)

小技巧:用 struct.calcsize() 可以提前计算格式字符串占用的字节数。比如 struct.calcsize('<I 2H f') 返回12,这样你就能知道需要读取多少字节。

实战:自定义二进制文件格式

有时候,你需要自己定义一种文件格式。比如保存一组传感器的数据:

import struct

def save_sensor_data(filename, readings):
    """保存传感器数据到二进制文件"""
    with open(filename, 'wb') as f:
        # 文件头:魔数(4字节) + 版本号(2字节) + 数据条数(4字节)
        magic = b'SENS'
        version = 1
        count = len(readings)
        f.write(struct.pack('<4s H I', magic, version, count))
        
        # 每条数据:时间戳(4字节) + 温度(4字节float) + 湿度(4字节float)
        for ts, temp, hum in readings:
            f.write(struct.pack('<I f f', ts, temp, hum))

def load_sensor_data(filename):
    """从二进制文件加载传感器数据"""
    readings = []
    with open(filename, 'rb') as f:
        # 读取文件头
        magic, version, count = struct.unpack('<4s H I', f.read(10))
        if magic != b'SENS':
            raise ValueError('不是有效的传感器数据文件')
        
        # 读取每条数据
        for _ in range(count):
            data = f.read(12)  # 4 + 4 + 4
            ts, temp, hum = struct.unpack('<I f f', data)
            readings.append((ts, temp, hum))
    
    return readings

# 使用示例
data = [(1000, 25.5, 60.0), (1001, 26.0, 59.5), (1002, 25.8, 61.0)]
save_sensor_data('sensor.bin', data)
loaded = load_sensor_data('sensor.bin')
print(loaded)

注意:自定义二进制格式一定要设计好「魔数」和「版本号」。魔数用来快速识别文件类型,版本号用来兼容后续升级。我见过太多项目,一开始没考虑版本兼容,后来数据格式一改,旧文件全废了。

避坑指南

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

  • 不要用文本模式处理二进制文件——Windows下 '\n' 会被自动转换成 '\r\n',图片会损坏。
  • 注意文件指针的位置——read() 之后指针会移动,如果需要重新读取,记得 seek(0)。
  • 大文件不要一次读完——用分块读取,或者用 memoryview 零拷贝处理。
  • struct的格式字符串要严格匹配——多一个字节少一个字节,解包就会报错。

我曾经在做一个嵌入式项目时,用二进制格式存储配置参数。因为结构体定义没对齐,导致在不同平台上读取的数据完全不一样。后来我强制在格式字符串里加了填充字节,问题才解决。

二进制文件操作,说白了就是「按规则读写字节」。规则清楚了,剩下的就是细心。你想想看,计算机底层不都是0和1吗?掌握了二进制操作,你就真正理解了文件系统的本质。