Python 语法篇 · 第11章

文件与数据持久化

语料、模型、配置都存在文件里。本章掌握文件读写、路径处理、JSON/CSV 数据处理与 with 上下文管理器。

🎯 学习目标

  • 掌握 open() 的各种模式与文本文件读写
  • 理解为什么要用 with,了解上下文管理器原理
  • 掌握 read/readline/readlines 与按行遍历大文件
  • 熟练使用现代路径库 pathlib 与目录操作
  • 掌握 JSON、CSV 两大数据格式的读写(NLP 语料最常用)
  • 了解 pickle 的用法与安全风险

1open 与文件打开模式

读写文件三步走:打开 → 操作 → 关闭。内置函数 open(路径, 模式, encoding=...) 返回文件对象。

模式名称文件不存在时说明
"r"只读(默认)报错 FileNotFoundError最常用
"w"只写新建覆盖已有全部内容(小心!)
"a"追加新建在文件末尾追加,不覆盖
"r+"读写报错不新建
b二进制"rb"/"wb"/"ab":读写图片、模型等,不带 encoding
t文本(默认)"rt"/"wt":读写 str,按编码转换
# 最原始的写法(不推荐,忘记 close 会占用资源)
f = open("demo.txt", "w", encoding="utf-8")
f.write("你好")
f.close()

# ✅ 推荐:with 语句,结束后自动关闭(哪怕中途异常也会关)
with open("demo.txt", "w", encoding="utf-8") as f:
    f.write("第一行\n")
    f.write("第二行\n")

Windows 处理中文文件,encoding="utf-8" 几乎必须写(否则默认 GBK,读别人写的 UTF-8 文件就乱码/报错)。二进制模式 "rb"/"wb" 处理的是 bytes,不能指定 encoding。

2读取文本文件

# 一次性读取整个文件为字符串
with open("demo.txt", "r", encoding="utf-8") as f:
    content = f.read()
print(content)

# 按行读取
with open("demo.txt", encoding="utf-8") as f:
    first = f.readline()          # 读一行(含末尾 \n)
    rest = f.readlines()          # 剩下所有行 → 列表 ['第二行\n']
print(repr(first))

# ✅ 最 Pythonic:文件对象本身可迭代,逐行读取省内存(大文件首选)
with open("big.txt", encoding="utf-8") as f:
    for line_no, line in enumerate(f, 1):
        line = line.strip()       # 去掉行尾换行和空白
        if line:                  # 跳过空行
            print(line_no, line)

几个 G 的日志/语料文件千万不要 read(),内存会爆。用 for line in f 逐行处理,内存占用恒定。

3写入文件

# write:写入字符串,返回字符数;不会自动加换行
lines = ["Python", "NLP", "jieba"]
with open("langs.txt", "w", encoding="utf-8") as f:
    for item in lines:
        f.write(item + "\n")

# writelines:写入字符串序列(注意:也不会自动加换行!)
with open("langs.txt", "w", encoding="utf-8") as f:
    f.writelines(x + "\n" for x in lines)

# 追加模式 a:日志场景
with open("run.log", "a", encoding="utf-8") as f:
    f.write("2026-10-25 程序启动\n")

4with 与上下文管理器原理

with 之所以能自动关闭,是因为文件对象实现了上下文管理协议(__enter__/__exit__)。等价于下面的 try/finally:

# with open(...) as f: ...  等价于:
f = open("demo.txt", encoding="utf-8")
try:
    content = f.read()
finally:
    f.close()          # 无论是否异常都保证关闭

# 自定义上下文管理器(了解)
class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        print(f"耗时 {time.time() - self.start:.4f} 秒")
        return False    # 返回 False:异常继续向外抛(不吞异常)

with Timer():
    total = sum(range(1_000_000))

更简单的方式是用 contextlib 写生成器版上下文管理器,第 12、14 章会再见到。

5路径处理:pathlib 现代写法

老式 os.path 是一堆字符串函数,Python 3.4+ 推荐用面向对象的 pathlib,跨平台、更直观。

from pathlib import Path

# 构造路径(自动适配 Windows 的 \ 和 Mac/Linux 的 /)
p = Path("data") / "raw" / "news.txt"     # 用 / 拼接路径,优雅!
print(p)                    # data\raw\news.txt (Windows)

# 常用属性/方法
p.name                      # 'news.txt'      文件名
p.stem                      # 'news'          不含扩展名
p.suffix                    # '.txt'          扩展名
p.parent                    # data\raw        所在目录
p.exists()                  # 是否存在
p.is_file(); p.is_dir()     # 是文件/目录
p.absolute()                # 绝对路径

# 读写文本(pathlib 直接封装好了)
Path("hello.txt").write_text("你好", encoding="utf-8")
text = Path("hello.txt").read_text(encoding="utf-8")

# 实用:脚本所在目录(不受运行位置影响)
BASE = Path(__file__).resolve().parent
data_path = BASE / "data" / "news.txt"
需求老式 os.pathpathlib
拼接os.path.join(a,b)a / b
存在os.path.exists(p)p.exists()
文件名os.path.basename(p)p.name
扩展名os.path.splitext(p)[1]p.suffix

6目录与文件操作

from pathlib import Path
import shutil, os

Path("output").mkdir(exist_ok=True)            # 创建目录(已存在不报错)
Path("a/b/c").mkdir(parents=True, exist_ok=True)  # 递归创建多级目录

# 遍历目录下的文件
for f in Path("data").iterdir():               # 直接子项
    print(f)
for f in Path("data").glob("*.txt"):           # 只匹配 .txt
    print(f)
for f in Path("data").rglob("*.csv"):          # 递归所有层级找 csv
    print(f)

# 文件移动/复制/删除(shutil 是高级文件操作库)
shutil.copy("a.txt", "backup/a.txt")           # 复制文件
shutil.copytree("dir1", "dir2")                # 复制整个目录
shutil.move("old.txt", "new.txt")              # 移动/重命名
os.remove("x.txt")                             # 删除文件
shutil.rmtree("tmp_dir")                       # 删除整个非空目录(谨慎!)

7JSON:最通用的数据交换格式

NLP 中标注语料、API 返回、配置文件大量使用 JSON。注意类型映射差异:

JSONPython
object {}dict
array []list
string(仅双引号)str
true / false / nullTrue / False / None
数字int / float
import json

article = {
    "id": 1,
    "title": "Python入门",
    "tags": ["编程", "教程"],
    "published": True,
    "views": None
}

# 写:Python 对象 → JSON 字符串/文件
json_str = json.dumps(article, ensure_ascii=False, indent=2)
print(json_str)
# ensure_ascii=False:中文原样输出(默认会转成 \uXXXX)
# indent=2:缩进美化

with open("article.json", "w", encoding="utf-8") as f:
    json.dump(article, f, ensure_ascii=False, indent=2)   # 注意是 dump 不是 dumps

# 读:JSON 文件 → Python 对象
with open("article.json", encoding="utf-8") as f:
    data = json.load(f)
print(data["tags"])            # ['编程', '教程']

data2 = json.loads('{"a": 1, "b": [true, null]}')   # loads:字符串 → 对象
print(data2)                   # {'a': 1, 'b': [True, None]}

JSONL(每行一个独立 JSON 对象)是 NLP 语料常见格式,不能整体 json.load:records = [json.loads(line) for line in open(f, encoding="utf-8") if line.strip()]。

8CSV:表格数据

import csv

# 写入(Excel 打开中文 csv 乱码时,用 encoding="utf-8-sig" 带 BOM)
with open("students.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.writer(f)
    writer.writerow(["姓名", "成绩"])
    writer.writerows([["张三", 90], ["李四", 85]])

# DictWriter:按字典写(列名不易错位,推荐)
with open("students2.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "score"])
    writer.writeheader()
    writer.writerow({"name": "王五", "score": 77})

# 读取
with open("students.csv", encoding="utf-8-sig") as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)             # ['姓名','成绩'] ['张三','90'](值都是字符串)

with open("students.csv", encoding="utf-8-sig") as f:
    for row in csv.DictReader(f):     # 每行是字典,用列名访问
        print(row["姓名"], row["成绩"])

打开 csv 文件统一加 newline=""(避免 Windows 多空行);复杂表格分析建议直接使用 pandas:pd.read_csv() / df.to_csv()。

9pickle:Python 对象的专属序列化

pickle 能把几乎任意 Python 对象(训练好的模型、复杂字典)序列化为字节,机器学习保存模型常用(joblib 基于它):

import pickle

model_data = {"vocab": {"我": 0, "爱": 1}, "acc": 0.92}

with open("model.pkl", "wb") as f:        # 二进制模式 wb
    pickle.dump(model_data, f)

with open("model.pkl", "rb") as f:
    loaded = pickle.load(f)
print(loaded["acc"])                      # 0.92

绝对不要 pickle.load 来源不可信的文件——反序列化时可执行任意代码,是严重安全漏洞。跨语言交换数据用 JSON/Parquet;保存 sklearn 模型可用 joblib,原理与风险相同。

10综合案例:语料文件批处理

from pathlib import Path
import json

def load_corpus(folder):
    """读取文件夹下所有 .txt,返回 [{file, text}, ...]"""
    records = []
    for path in Path(folder).glob("*.txt"):
        text = path.read_text(encoding="utf-8")
        records.append({"file": path.name, "text": text.strip()})
    return records

def save_jsonl(records, out_path):
    """把语料保存为 JSONL(每行一个 JSON)。"""
    with open(out_path, "w", encoding="utf-8") as f:
        for r in records:
            f.write(json.dumps(r, ensure_ascii=False) + "\n")

def load_jsonl(path):
    with open(path, encoding="utf-8") as f:
        return [json.loads(line) for line in f if line.strip()]

# corpus = load_corpus("data/raw")
# save_jsonl(corpus, "data/corpus.jsonl")
# for r in load_jsonl("data/corpus.jsonl"):
#     print(r["file"], len(r["text"]))

11本章小结与练习

📌 关键结论

  • 文本文件一律 with open(..., encoding="utf-8");w 覆盖、a 追加。
  • 大文件逐行 for line in f,不要 read() 全读。
  • 路径操作用 pathlib 的 Path / 写法;遍历用 glob/rglob。
  • JSON 用 dump/load(文件)、dumps/loads(字符串),中文加 ensure_ascii=False。
  • pickle 方便但只加载可信文件。
练习1:打开文件时 "w" 和 "a" 模式有什么区别?为什么推荐 with?
w 会清空覆盖已有内容从头写,a 在末尾追加且不覆盖;文件不存在时两者都会新建。with 通过上下文管理协议在代码块结束(含异常)时自动调用 close,避免忘记关闭导致的资源泄漏与数据未刷盘。
练习2:json.dumps 默认输出中文是什么样?如何让中文正常显示?JSONL 怎么读取?
默认 ensure_ascii=True,中文会被转义成 \uXXXX 形式;传 ensure_ascii=False 可原样保留中文,indent=2 可美化。JSONL 每行是独立 JSON,需逐行遍历文件,对每个非空行调用 json.loads(line),收集成列表,而不能整体 json.load。
练习3:统计一个文本文件中每个字出现的次数,把结果按次数降序写入 result.txt。
with open 逐行读或 read 全文;用 collections.Counter(文本) 统计(可先去掉空白);sorted(c.items(), key=lambda x:x[1], reverse=True) 排序;再 with open(result,"w",encoding="utf-8") 逐行 f.write(f"{字},{次数}\n")。全部用 utf-8 编码。