第三节:二进制文件操作——图片、音频等二进制文件的读写

聊完了文本文件,咱们来碰碰真正的硬骨头——二进制文件。

你想想看,一张图片、一段音频、一个视频,它们本质上是什么?是一串0和1。文本文件好歹还有个编码规则(UTF-8、GBK),二进制文件可不管这些,它只认字节。

我个人习惯把二进制文件比作「原始食材」。文本文件是切好、配好料的半成品,而二进制文件就是刚从地里拔出来的萝卜——你得直接上手处理。

3.1 为什么需要二进制模式?

Python 默认用文本模式打开文件。但处理图片、音频时,必须用 rb(读二进制)或 wb(写二进制)。

为什么?因为文本模式会做「翻译」工作。比如 Windows 下,\n 会被翻译成 \r\n。对于文本文件这没问题,但对于图片——你想想,图片里某个字节恰好是 0x0A(也就是 \n),被翻译后整个文件就坏了。

⚠️ 我曾经踩过这个坑: 刚入行时,我用文本模式读了一张 PNG 图片,结果图片死活打不开。查了半天,发现文件大小都变了。从那以后,处理非文本文件我必用 rb / wb。

3.2 核心操作:rb 和 wb

说白了,二进制读写就两件事:读进来是 bytes,写出去也是 bytes。

3.2.1 读取二进制文件

with open('photo.jpg', 'rb') as f:
    data = f.read()
    print(type(data))  # <class 'bytes'>
    print(len(data))   # 文件字节数

看到没?data 是 bytes 类型,不是字符串。你不能对它做 .encode() 或 .decode()(除非你知道编码规则)。

3.2.2 写入二进制文件

with open('copy.jpg', 'wb') as f:
    f.write(data)

就这么简单。把读出来的字节原封不动写进去,就是文件复制。

💡 小技巧: 我经常用这种方式做文件备份。一行代码搞定,比用 shutil 还直观。

3.3 实战:图片复制工具

咱们写个完整的例子。把一张图片复制成新文件,同时打印一些元信息。

def copy_image(src_path, dst_path):
    try:
        with open(src_path, 'rb') as src:
            data = src.read()
        
        with open(dst_path, 'wb') as dst:
            dst.write(data)
        
        print(f'✅ 复制成功!')
        print(f'   源文件: {src_path}')
        print(f'   目标文件: {dst_path}')
        print(f'   文件大小: {len(data)} 字节')
    
    except FileNotFoundError:
        print(f'❌ 源文件不存在: {src_path}')
    except PermissionError:
        print(f'❌ 没有权限访问文件')
    except Exception as e:
        print(f'❌ 发生未知错误: {e}')

# 使用示例
copy_image('input.png', 'output.png')

嗯,这里要注意:try-except 不是摆设。我在项目中遇到过用户给了个不存在的路径,程序直接崩溃。加个异常处理,用户体验好很多。

3.4 逐块读写:处理大文件

如果文件很大(比如几百 MB 的视频),一次性 f.read() 会吃掉大量内存。怎么办?逐块读写。

def copy_large_file(src_path, dst_path, chunk_size=1024*1024):
    """逐块复制大文件,默认每块 1MB"""
    with open(src_path, 'rb') as src, open(dst_path, 'wb') as dst:
        while True:
            chunk = src.read(chunk_size)
            if not chunk:
                break
            dst.write(chunk)
    
    print(f'✅ 大文件复制完成')

# 复制一个视频文件
copy_large_file('video.mp4', 'video_backup.mp4')

为什么用 while True?因为 read(chunk_size) 读到文件末尾会返回空字节 b'',这就是退出条件。

🔑 关键点: 逐块读写是处理大文件的标配。我做过一个项目,需要处理 2GB 的日志文件(二进制格式),不用逐块读写,内存直接爆掉。

3.5 二进制文件的定位:seek 和 tell

有时候你不想从头读起。比如一个音频文件,你想跳过前 44 字节的 WAV 文件头,直接读音频数据。

with open('audio.wav', 'rb') as f:
    # 查看当前位置
    print(f.tell())  # 0,文件开头
    
    # 跳过文件头(WAV 头通常 44 字节)
    f.seek(44)
    print(f.tell())  # 44
    
    # 读取音频数据
    audio_data = f.read(1024)
    
    # 回到开头
    f.seek(0)
    print(f.tell())  # 0

seek(offset, whence) 的第二个参数:

whence 值 含义 示例
0 文件开头(默认) f.seek(100, 0)
1 当前位置 f.seek(50, 1)
2 文件末尾 f.seek(-10, 2)
⚠️ 注意: 文本模式下 seek 的行为很诡异(因为编码问题)。二进制模式下,seek 和 tell 才可靠。我建议:需要定位操作时,一律用二进制模式。

3.6 实战:读取图片的二进制头部信息

每张图片都有文件头,里面藏着格式信息。比如 PNG 的前 8 个字节是固定签名。

def check_png_signature(file_path):
    """检查文件是否为 PNG 格式"""
    png_signature = b'\x89PNG\r\n\x1a\n'
    
    with open(file_path, 'rb') as f:
        header = f.read(8)
    
    if header == png_signature:
        print('✅ 这是一个 PNG 文件')
        return True
    else:
        print(f'❌ 不是 PNG 文件,头部为: {header.hex()}')
        return False

# 测试
check_png_signature('test.png')

你想想看,这种技巧有什么用?我在做文件上传系统时,就用它来校验用户上传的是不是真正的图片,而不是伪装成图片的恶意文件。

3.7 二进制文件操作总结

来,咱们捋一下核心要点:

  • 模式选择:非文本文件一律用 rb / wb
  • 数据类型:读写都是 bytes,不是 str
  • 大文件处理:用 read(chunk_size) 逐块读写
  • 定位操作:seek 和 tell 在二进制模式下最可靠
  • 异常处理:文件操作必须加 try-except
🎯 一句话总结: 二进制文件操作就是「字节的搬运工」。你只管把字节读进来,再原封不动写出去。别做多余的事,别用文本模式。
二进制文件操作知识体系 二进制文件操作 模式选择 rb / wb 数据类型 bytes 大文件逐块读写 避免文本模式 异常处理必备 不可直接 decode 原样搬运字节 read(chunk_size) while True 循环 核心:字节的搬运工,别做多余的事
可可精品资料库