Python 文件操作实战:pathlib、上下文管理器与 WAV 文件读写

2026-05-26 30 预计阅读时间:1 分钟
来源:realpython.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:9 分钟

Python 处理文件的方式远不止 open() 一行调用。从路径操作到二进制音频文件读写,每个环节都有容易踩坑的细节。这篇文章把 pathlib、上下文管理器和 WAV 文件处理串起来,用可运行的代码讲清楚该注意什么。

pathlib:比 os.path 更安全的路径操作

os.path 拼路径时,斜杠方向、类型转换、重复拼接都是常见出错点。pathlib.Path 用面向对象的方式解决这些问题:

from pathlib import Path

# 创建项目目录结构
project = Path("my_project")
data_dir = project / "data"          # 用 / 拼接,比 os.path.join 更直观
output_file = data_dir / "result.wav"

# 一次性创建多层目录(exist_ok=True 防止重复创建报错)
data_dir.mkdir(parents=True, exist_ok=True)

# 常用属性一览
print(output_file.suffix)     # '.wav'
print(output_file.stem)       # 'result'
print(output_file.parent)     # my_project/data
print(output_file.name)       # 'result.wav'

# 批量查找所有 WAV 文件
wav_files = list(data_dir.glob("**/*.wav"))
for f in wav_files:
    print(f.resolve())        # 绝对路径

几个实际开发中的建议:

  • 始终用 Path 而不是字符串拼路径——Path 对象在 Windows 和 Linux 上自动处理斜杠方向,省掉 os.path.join 和手动加 / 的混乱。
  • resolve()absolute() 更可靠——resolve() 会消除符号链接和 ..,返回真正的绝对路径;absolute() 只做简单拼接。
  • 读写前先检查——Path.exists()Path.is_file() 是最轻量的守卫,比直接 open 再捕获异常更清晰。

上下文管理器:不只是 with open()

with 语句的核心价值是保证资源释放,不管代码是否抛异常。文件是最常见的场景,但原理适用于任何需要清理的资源:

# 标准用法:读文件
with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()
# f 已自动关闭,无需手动 close()

# 写文件时同样可靠
with open("output.txt", "w", encoding="utf-8") as f:
    f.write("处理完成\n")
# 即使 write 抛异常,文件也会正确关闭,不会留下半写状态

# 同时打开两个文件:从输入复制到输出
with open("input.wav", "rb") as src, open("copy.wav", "wb") as dst:
    dst.write(src.read())

自定义上下文管理器也很实用,比如临时切换工作目录:

import os
from contextlib import contextmanager

@contextmanager
def temp_directory(path):
    """临时切换到指定目录,退出时恢复原目录"""
    original = os.getcwd()
    os.chdir(path)
    try:
        yield path
    finally:
        os.chdir(original)

# 使用:在特定目录下批量处理文件,结束后自动回到原目录
with temp_directory("/tmp/audio_processing"):
    for wav in Path(".").glob("*.wav"):
        print(f"正在处理: {wav}")

关键点:finally 块里的清理逻辑一定会执行,这是 withtry/finally 手写更安全的原因——你不会忘记写 finally

WAV 文件读写:二进制结构不能随便拼

WAV 是一种 RIFF 格式的二进制文件,头部有固定结构:44 字节的 metadata(采样率、位深度、声道数等)后面紧跟原始音频数据。用 struct 模块手动读写是最底层的方式,也是最值得理解的方式:

import struct
import array

def write_wav(filename, samples, sample_rate=44100, num_channels=1, bits_per_sample=16):
    """将整数采样列表写入标准 16-bit PCM WAV 文件"""
    num_samples = len(samples)
    data_size = num_samples * num_channels * (bits_per_sample // 8)

    with open(filename, "wb") as f:
        # RIFF 头 (12 bytes)
        f.write(b"RIFF")
        f.write(struct.pack("<I", 36 + data_size))   # 文件总大小 - 8
        f.write(b"WAVE")

        # fmt 子块 (24 bytes)
        f.write(b"fmt ")
        f.write(struct.pack("<I", 16))                # fmt 块数据大小
        f.write(struct.pack("<H", 1))                 # PCM 格式
        f.write(struct.pack("<H", num_channels))
        f.write(struct.pack("<I", sample_rate))
        f.write(struct.pack("<I", sample_rate * num_channels * bits_per_sample // 8))  # 字节率
        f.write(struct.pack("<H", num_channels * bits_per_sample // 8))  # 块对齐
        f.write(struct.pack("<H", bits_per_sample))

        # data 子块
        f.write(b"data")
        f.write(struct.pack("<I", data_size))

        # 写入采样数据
        for s in samples:
            f.write(struct.pack("<h", s))  # 16-bit signed little-endian

def read_wav(filename):
    """读取 16-bit PCM WAV 文件,返回采样列表和元信息"""
    with open(filename, "rb") as f:
        riff = f.read(4)
        assert riff == b"RIFF", f"不是 RIFF 格式: {riff}"
        file_size = struct.unpack("<I", f.read(4))[0]
        wave = f.read(4)
        assert wave == b"WAVE", f"不是 WAVE 格式: {wave}"

        # 读取 fmt 块
        fmt_id = f.read(4)
        fmt_size = struct.unpack("<I", f.read(4))[0]
        fmt_data = f.read(fmt_size)

        audio_format = struct.unpack("<H", fmt_data[0:2])[0]
        num_channels = struct.unpack("<H", fmt_data[2:4])[0]
        sample_rate = struct.unpack("<I", fmt_data[4:8])[0]
        bits_per_sample = struct.unpack("<H", fmt_data[14:16])[0]

        # 读取 data 块
        data_id = f.read(4)
        data_size = struct.unpack("<I", f.read(4))[0]
        num_samples = data_size // (bits_per_sample // 8)

        raw = f.read(data_size)
        samples = list(struct.unpack(f"<{num_samples}h", raw))

        return {
            "sample_rate": sample_rate,
            "num_channels": num_channels,
            "bits_per_sample": bits_per_sample,
            "audio_format": audio_format,
            "samples": samples,
        }

# 生成 1 秒 440Hz 正弦波并写入 WAV
import math

duration = 1.0
sample_rate = 44100
samples = [
    int(32767 * math.sin(2 * math.pi * 440 * i / sample_rate))
    for i in range(int(sample_rate * duration))
]

write_wav("tone_440hz.wav", samples, sample_rate=sample_rate)
info = read_wav("tone_440hz.wav")
print(f"采样率: {info['sample_rate']}, 声道: {info['num_channels']}, "
      f"位深度: {info['bits_per_sample']}, 采样数: {len(info['samples'])}")

运行前注意:

  • samples 列表中的值必须在 -3276832767 之间(16-bit signed 范围),超出会 struct.pack 报错。
  • 这段代码只处理最常见的 PCM 格式(audio_format=1),遇到浮点格式或压缩格式会失败。
  • 生产环境建议用 wave 标准库模块或 soundfile 第三方库,它们处理更多边界情况。

实战检查清单

把上面三个主题合在一起,日常文件操作可以按这个清单走:

场景 推荐做法 避坑点
拼路径 Path() / "dir" / "file" 别用字符串 + 拼斜杠
读文本 with open(..., encoding="utf-8") 不指定 encoding 在 Windows 上默认用 GBK
写二进制 with open(..., "wb") 别用 "w" 写二进制,会被当文本处理
批量查找 Path.glob("**/*.wav") ** 递归搜索可能很慢,大目录加限制
WAV 读写 wave 模块或 struct 手写 44 字节头写错一个字段文件就废了
临时资源 @contextmanager + with 别依赖 __del__ 做清理,时机不可控

最后一条:任何 open 都应该配 with。裸调用 f = open(...) 然后忘记 f.close() 是最常见的文件句柄泄漏来源,在长期运行的服务里会慢慢耗尽文件描述符。养成习惯,看到 open 就写 with,没有例外。


相关推荐