《Python AI 应用开发入门》第 3.5 节。
使用 with、UTF-8 和 pathlib 安全读写文本文件与本地提示词模板。
本节目标
学完本节后,你应当能够:
- 2. 使用
pathlib.Path 构造跨平台路径。
1. 文件路径为什么容易出错
下面代码中的相对路径:
open("data/prompt.txt")
通常以当前工作目录为起点,而不是自动以 .py 文件所在目录为起点。
观察当前工作目录:
from pathlib import Pathprint(Path.cwd())
如果从不同目录启动同一个程序,相对路径可能指向不同位置。
2. 使用 pathlib
Path 用 / 组合路径,兼容 macOS、Linux 和 Windows:
from pathlib import Pathdata_dir = Path("chat_app") / "data"prompt_path = data_dir / "prompt.txt"print(prompt_path)
常用属性与方法:
print(prompt_path.name)print(prompt_path.suffix)print(prompt_path.parent)print(prompt_path.exists())print(prompt_path.is_file())
不要手工拼接:
# path = "chat_app/" + "data/" + "prompt.txt"
不同操作系统的路径分隔符可能不同。
3. 相对于模块定位资源
包内资源通常相对于当前模块文件定位:
from pathlib import PathPACKAGE_ROOT = Path(__file__).resolve().parentDATA_DIR = PACKAGE_ROOT / "data"PROMPT_PATH = DATA_DIR / "prompt.txt"
无论终端从哪里启动,PROMPT_PATH 都会指向 chat_app/data/prompt.txt。
但命令行参数中由用户提供的相对路径,通常应继续相对于当前工作目录解释。选择哪种起点取决于路径含义。
4. 使用 with 打开文件
with 是 Python 用来管理资源的语句。文件、网络连接等资源在使用结束后需要释放,with 可以自动完成进入时的准备和离开时的清理。
基本结构是:
with 要管理的对象 as 变量: # 在缩进代码块中使用这个变量
用在文件操作中时:
- •
path.open(...) 打开文件并返回文件对象。 - •
as file 把这个文件对象保存到变量 file。
完整示例:
from pathlib import Pathpath = Path("notes.txt")with path.open(mode="r", encoding="utf-8") as file: content = file.read()print(content)
with 管理的这段运行范围称为上下文,能够这样配合 with 使用的对象称为上下文管理器。初学阶段不需要自己实现上下文管理器,只需要知道 open() 返回的文件对象支持这种用法。
即使读取过程中出现异常,离开 with 代码块时文件仍然会被关闭。代码块之外不要继续读写 file,因为它已经关闭。
不使用 with 时必须自己保证关闭:
file = path.open(mode="r", encoding="utf-8")try: content = file.read()finally: file.close()
因此文本文件优先使用 with。
5. 文件模式
最需要警惕的是 "w":文件一打开,原内容就会被截断。
覆盖写入:
with path.open(mode="w", encoding="utf-8") as file: file.write("新的完整内容\n")
追加:
with path.open(mode="a", encoding="utf-8") as file: file.write("新增一行\n")
6. 快捷读写方法
小型文本可以使用:
path.write_text("你好,文件!\n", encoding="utf-8")content = path.read_text(encoding="utf-8")
它们内部会自动打开和关闭文件。
write_text() 同样会覆盖原内容。写入前确认目标路径。
7. 创建目录
写入嵌套路径前确保父目录存在:
from pathlib import Pathhistory_path = Path("chat_app") / "data" / "history.txt"history_path.parent.mkdir(parents=True, exist_ok=True)history_path.write_text("聊天开始\n", encoding="utf-8")
- •
parents=True:缺少的上级目录一起创建。 - •
exist_ok=True:目录已存在时不报错。
不要把用户输入未经检查就直接当成任意系统路径写入。
8. 编码
始终明确文本编码:
with path.open(mode="r", encoding="utf-8") as file: content = file.read()
省略编码会使用系统默认值,不同电脑可能不一致。中文内容尤其容易出现乱码或 UnicodeDecodeError。
读取来源不明的文件时,不要立即使用 errors="ignore"。它可能静默丢失字符。先确认文件真实编码。
9. 换行
写入多行:
lines = [ "[user] 你好", "[assistant] 你好!",]text = "\n".join(lines) + "\n"path.write_text(text, encoding="utf-8")
读取后拆行:
content = path.read_text(encoding="utf-8")lines = content.splitlines()
splitlines() 能处理不同平台的常见换行形式,并且不会在每一项末尾保留换行符。
10. 逐行读取
file.read() 会把全部内容放入内存。大文件可以逐行处理:
with path.open(mode="r", encoding="utf-8") as file: for line_number, line in enumerate(file, start=1): cleaned_line = line.rstrip("\n") print(f"{line_number}: {cleaned_line}")
文件对象本身是可迭代对象,不需要先调用 readlines()。
只去除换行时使用 rstrip("\n")。直接 strip() 还会删除两端其他空格,可能改变原文本含义。
11. 读取 Prompt 模板
创建 chat_app/data/prompt_template.txt:
你是一名{role}。请用{style}风格解释:{topic}要求:{rules}
读取和填充:
from pathlib import Pathdef load_prompt_template(path: Path) -> str: return path.read_text(encoding="utf-8")def render_prompt( template: str, role: str, style: str, topic: str, rules: list[str],) -> str: numbered_rules = [] for index, rule in enumerate(rules, start=1): numbered_rules.append(f"{index}. {rule}") return template.format( role=role, style=style, topic=topic, rules="\n".join(numbered_rules), )
模板和代码分离后,可以修改措辞而不改 Python 逻辑。
模板字段缺失时,format() 可能触发 KeyError。入口层应把它转成清楚的配置错误。
12. 处理文件异常
from pathlib import Pathdef load_text(path: Path) -> str: try: return path.read_text(encoding="utf-8") except FileNotFoundError as error: raise ValueError(f"文件不存在:{path}") from error except PermissionError as error: raise ValueError(f"没有权限读取:{path}") from error except UnicodeDecodeError as error: raise ValueError(f"文件不是有效 UTF-8:{path}") from error
这里把底层文件异常转换为业务层 ValueError,并保留异常链。
是否转换取决于模块接口。若调用者需要分别处理 FileNotFoundError 和 PermissionError,就让原异常传播。
13. 写入前的安全检查
写文件前确认:
JSON 章节会使用“临时文件写完后替换”的方式,降低写入一半造成文件损坏的风险。
14. 文本聊天记录
from pathlib import Pathdef append_message(path: Path, role: str, content: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open(mode="a", encoding="utf-8") as file: file.write(f"[{role}] {content}\n")
它适合人类阅读,但不容易可靠恢复为结构化字典。下一节会使用 JSON 保存完整消息结构。
动手练习
练习 1:文件信息
接收一个路径,使用 Path 输出名称、后缀、父目录、是否存在以及是否为文件。不存在时不要崩溃。
练习 2:Prompt 模板
创建文本模板并编写 load_prompt_template() 与 render_prompt()。测试正常模板和缺少字段两种情况。
练习 3:聊天文本导出
编写 export_history(messages, path):
随堂小测
- 5. 为什么要明确
encoding="utf-8"? - 6.
splitlines() 与 strip() 的目的有什么区别?
参考答案
- 2. 使用
Path(__file__).resolve().parent 作为模块目录,再组合相对路径。 - 3. 离开代码块时自动关闭文件,包括发生异常的情况。
- 5. 避免依赖不同操作系统的默认编码,保证中文可移植。
- 6.
splitlines() 按换行拆分;strip() 删除文本两端空白。 - 9. 角色和内容等字段边界不够明确,转义和恢复容易出错。
本节完成检查