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,计算平均温度,并把结果追加到 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,说明某行不符合“日期,数值”的格式,应在解析处增加行号提示以便定位。掌握这些细节后,文本读写就不再是“能跑就行”的代码,而是可移植、可排查、可复现的数据处理环节。