2. 文本文件基础读写

在科学计算中,文本文件是最常见的数据载体之一:实验参数、观测记录、日志、CSV 表格、配置项,往往都以纯文本形式保存。Python 通过内置的 open() 函数统一处理这些文件,但要真正把读写做对,需要同时理解四个要素:打开模式决定能做什么,编码决定字节如何变成字符,读取/写入方式决定数据如何进出,上下文管理器决定资源是否被可靠释放。本节围绕这四点展开,并给出可直接运行的示例。

open 函数与文件模式

open(file, mode='r', encoding=None) 返回一个文件对象。file 可以是相对或绝对路径;mode 是字符串,由若干字符组合而成。最常用的模式如下:

模式含义文件不存在时写入位置
'r'只读文本抛出 FileNotFoundError—
'w'只写文本,清空原内容新建文件从头开始
'a'追加文本新建文件文件末尾
'x'独占创建,已存在则报错新建文件从头开始
'r+'读写,不清空抛出 FileNotFoundError从头开始

需要特别警惕 'w':它会立即截断已有文件。若想保留历史数据,应使用 'a';若想防止误覆盖,可用 'x',文件已存在时程序会明确报错,而不是悄悄清空。模式后加 'b'(如 'rb')表示二进制模式,此时读写的是 bytes,不能指定 encoding。科学计算中读取图像、压缩包、NumPy 的 .npy 等二进制文件时才使用二进制模式,普通文本数据一律用文本模式。

编码处理:显式指定,避免平台差异

文本模式下,Python 需要在字节与字符之间转换,这个规则就是编码。若不写 encoding,Python 会使用平台默认编码:Linux/macOS 通常是 UTF-8,而部分 Windows 环境是 GBK。同一份含中文的文件,在开发机上正常、在服务器上却报 UnicodeDecodeError,根源往往就在这里。因此工程上应养成显式指定编码的习惯:

with open("measurements.txt", "r", encoding="utf-8") as f:
    text = f.read()

写入时同样要指定 encoding="utf-8"。若读取的文件编码不确定,可以先用 errors="replace" 或 errors="ignore" 做容错读取以观察内容,但正式处理数据时应查明真实编码,而不是长期依赖容错参数,否则会静默丢失字符。另一个常见细节是换行符:文本模式默认启用通用换行,读取时 \r\n 会被统一转换为 \n;写入时可通过 newline="" 控制是否转换,处理 CSV 时通常建议加上它以避免多余空行。

逐行读取与写入

文件对象本身是可迭代的,直接 for line in f 就是最推荐的逐行读取方式:它按需加载,内存占用与文件大小无关,适合处理大文件。每行末尾保留 \n,需要时用 line.rstrip("\n") 去掉。下面示例读取一个记录温度观测的文本文件,跳过注释行与空行,并解析为数值:

records = []
with open("temps.txt", "r", encoding="utf-8") as f:
    for line in f:
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        day, value = line.split(",")
        records.append((day, float(value)))

print(records[:3])

假设 temps.txt 内容为 # 日期,温度、2024-05-01,21.5、2024-05-02,23.0,则输出前三条记录中的前两条解析结果。写入时,write() 不会自动添加换行,需要手动写入 \n;writelines() 接收字符串序列,同样不补换行。若要把计算结果逐行落盘,可这样写:

with open("result.txt", "w", encoding="utf-8") as f:
    for day, value in records:
        f.write(f"{day}\t{value:.2f}\n")

这里用制表符分隔、保留两位小数,便于后续用表格软件或 pandas.read_csv(sep="\t") 读取。注意 write() 的参数必须是字符串,写数值前要显式转换,否则会抛 TypeError。

上下文管理器:让关闭动作必然发生

文件打开后占用操作系统句柄,写入的数据还可能停留在缓冲区。若中途抛异常而没有关闭,轻则句柄泄漏,重则数据未落盘。with 语句保证离开代码块时一定调用 close(),即使块内发生异常也不例外。因此除极少数需要长期持有句柄的场景外,都应使用 with open(...) as f。若确实需要手动管理,也应写成 try/finally 确保关闭。

不要用 f = open(...) 后忘记 f.close();在循环中反复打开而不关闭,可能很快耗尽文件句柄。也不要对同一个文件同时用 'w' 打开两次,后一次会截断前一次尚未写完的内容。

下图把一次完整的“读取—解析—写回”过程按数据流与资源状态组织起来,帮助看清模式、编码、逐行处理与关闭动作之间的位置关系:

文本文件读写:数据流与资源状态 磁盘文件 temps.txt UTF-8 字节序列 解码 decode encoding="utf-8" 字节 → 字符 逐行迭代 for line in f strip / split / float 内存数据 records 列表 (日期, 温度) 逐行写入 f.write(f"...\n") mode="w" / "a" 编码 encode 字符 → 字节 写入缓冲区 result.txt 关闭时 flush 数据落盘 with open(...) as f:进入时打开句柄 → 块内读写 → 离开时自动 close() 并 flush,异常时同样关闭

一个可复现的完整示例

把上述要点串起来:读取 temps.txt,计算平均温度,并把结果追加到 summary.txt。示例数据为教学假设,运行结果取决于实际文件内容。

from pathlib import Path

src = Path("temps.txt")
if not src.exists():
    raise FileNotFoundError(f"缺少输入文件: {src}")

values = []
with open(src, "r", encoding="utf-8") as f:
    for line in f:
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        _, v = line.split(",")
        values.append(float(v))

if values:
    avg = sum(values) / len(values)
    with open("summary.txt", "a", encoding="utf-8") as out:
        out.write(f"样本数={len(values)}, 平均温度={avg:.2f}\n")
    print(f"已写入 {len(values)} 条样本的平均值 {avg:.2f}")
else:
    print("没有有效数据,未写入")

判断是否成功,可以看三点:控制台是否打印样本数与平均值;summary.txt 是否新增一行且格式为“样本数=…, 平均温度=…”;重复运行后该文件是否追加而非被覆盖。若出现 UnicodeDecodeError,优先检查文件真实编码;若出现 ValueError,说明某行不符合“日期,数值”的格式,应在解析处增加行号提示以便定位。掌握这些细节后,文本读写就不再是“能跑就行”的代码,而是可移植、可排查、可复现的数据处理环节。