文件、路径、流、临时文件与目录操作
本节目标
查询 Python pathlib、文件流、编码、临时文件、目录操作和清理边界。
文件代码先分清路径计算、字节与文本、资源所有权和操作系统语义。以下分别以 pathlib、open()、io、tempfile、shutil 和 os.path 的 Python 3.14 文档为准。选择模块时也可先回顾标准库地图;文本值和 Unicode 的边界见文本处理、正则表达式与 Unicode。
PathLike、纯路径与具体路径
调用方可把实现 os.PathLike 的对象交给接受路径的 API,但要区分只描述路径和实际碰文件系统的对象。Path 是具体路径对象,会执行文件系统 I/O;纯路径只计算路径语义,不访问文件系统。组合、比较或拆分路径无需触碰系统,应选 PurePath;确实要访问当前平台文件系统时再选 Path。
from pathlib import PurePath
archive_name = PurePath("archive") / "lesson.txt"
PurePath 不能替代 Path.exists()、打开文件或权限检查;反过来,构造 Path 也不证明路径存在、可访问或属于允许的根目录。把路径字符串的计算误当作文件系统授权,是常见的边界错误。
open、文本模式与二进制模式
open() 的模式决定流是文本还是二进制,也决定读写、追加或独占创建等能力;打开后的流由调用方关闭,优先用 with 表达所有权。有字符语义的内容应以明确编码的文本模式处理,协议、哈希或图像等原始数据才适合二进制模式。
from pathlib import Path
with Path("lesson.txt").open("w", encoding="utf-8", newline="\n") as stream:
stream.write("Python 标准库\n")
文本模式会在 str 与字节之间按编码转换,并按 newline 参数翻译换行;二进制模式读写的是 bytes,不能直接写入 str。遗漏 encoding 会依赖环境默认值,而在二进制流传入文本或在关闭后继续使用流都会失败。
编码、错误策略与换行
编码、errors 和 newline 是文本文件与外部字节之间的合同:encoding 决定转换,errors 决定无法转换时抛错或如何恢复,newline 控制通用换行处理。可交换的文本数据通常显式写 utf-8;replace 或 ignore 会损失信息,只能在调用方接受该后果时选用。
from pathlib import Path
text = Path("lesson.txt").read_text(encoding="utf-8", errors="strict")
errors="ignore" 会静默丢失信息,replace 会引入替代字符;两者都不适合作为未知损坏数据的无声修复。不要把磁盘上的字节长度当作字符数,也不要假设不同平台的默认换行相同。
io 流层次与缓冲
io 把底层原始字节流、缓冲字节流和文本流分层:缓冲层管理读取策略,文本层再管理编码和换行;内存文本可用 StringIO,不拥有文件描述符。数据边界决定所需的最低层级,只有调用方确实需要原始字节接口时才应绕过文本层。
from io import StringIO
buffer = StringIO("Python 标准库")
StringIO 保存的是内存中的 str,不是字节文件;对它调用 .buffer 或期待操作系统文件描述符都不成立。混用缓冲层和包装它的文本层时,未刷新缓冲便从另一层读取会得到意外的可见内容。
定位、游标与截断
只有可定位的流才适合 seek()、tell() 和 truncate();流对象是否可定位应由 seekable() 和其 API 合同决定。修改游标或截断前先确认流类型,文本流则只使用它明确支持的定位方式。文本流的 tell() 返回的位置不必是底层文件的原始字节偏移。
from io import StringIO
stream = StringIO("abc")
stream.seek(1)
stream.truncate(2)
网络管道等不可定位流会拒绝定位,文本编码和换行状态也让任意字节偏移不安全。把 tell() 的值当作跨进程、跨编码可复用的字节地址,或在错误位置截断,都会破坏数据。
目录遍历与 glob
iterdir()、glob() 和 rglob() 返回的是文件系统观察结果,顺序不是稳定 API 合同。调用方应限定遍历根目录、模式及链接策略,并在输出、测试或比较前显式排序。
from pathlib import Path
entries = sorted(path.as_posix() for path in Path("notes").glob("*.txt"))
目录可能在遍历期间变化,匹配结果也可能包含不预期的类型或权限错误。不要依赖目录枚举顺序,或将递归 glob 直接用于不可信的大树而没有资源和授权限制。
复制、移动、替换与元数据
复制、移动和替换分别有数据、元数据、命名空间与平台语义:copy2() 尽力复制数据和元数据,Path.rename() 与 Path.replace() 委托目标文件系统的重命名规则。API 的选择取决于是否保留元数据、是否覆盖目标以及如何恢复,最终语义还须在目标平台验证。
from pathlib import Path
from shutil import copy2
copied = Path(copy2("lesson.txt", "archive/lesson.txt"))
跨文件系统或平台时,rename() 与 replace() 的成功、原子性和替换语义不能作超出目标系统保证的承诺。copy2() 也不是所有平台元数据的可移植备份;把移动当成普适的原子提交,或忽略目标已存在的行为,会留下错误恢复漏洞。
临时文件与临时目录
临时资源应由创建它的作用域拥有,退出时完成关闭与清理;需要名字、匿名文件或目录时分别选择相应 tempfile 接口。上下文管理器能把临时文件的生命周期限制在使用范围内,临时绝对路径则不应进入输出、配置或长期状态。TemporaryDirectory 拥有目录并在上下文退出时清理。
from tempfile import TemporaryDirectory
with TemporaryDirectory() as temporary_name:
report_name = f"{temporary_name}/report.txt"
NamedTemporaryFile 在不同平台上重新打开和删除的限制不同,不能假定任意时刻都可按同一名字访问。也不要把上下文退出后的临时路径交给异步后台工作;资源所有权已结束,清理可能已发生。
from pathlib import Path
from shutil import copy2
from tempfile import TemporaryDirectory
with TemporaryDirectory() as temporary_name:
root = Path(temporary_name)
notes = root / "notes"
archive = root / "archive"
notes.mkdir()
archive.mkdir()
source = notes / "lesson.txt"
source.write_text("Python 标准库", encoding="utf-8", newline="\n")
target = Path(copy2(source, archive / source.name))
entries = sorted(path.relative_to(root).as_posix() for path in root.rglob("*.txt"))
text = source.read_text(encoding="utf-8")
copied = target.read_text(encoding="utf-8") == text
cleaned = not root.exists()
print(f"entries={entries}")
print(f"text={text}")
print("encoding=utf-8")
print(f"copied={copied}")
print(f"cleaned={cleaned}")
entries=['archive/lesson.txt', 'notes/lesson.txt']
text=Python 标准库
encoding=utf-8
copied=True
cleaned=True
报告只在临时目录中写入、复制、排序遍历和读取文本,并在退出后确认清理;它既不泄漏绝对路径,也不把临时目录当作持久存储。
路径安全、解析与符号链接
处理不可信路径时,授权应基于受控目录句柄、操作系统权限和不跟随链接等实际文件系统操作,而不是仅比较字符串;解析后的路径也可能在检查和使用之间改变。安全边界必须同时覆盖根目录限制、符号链接策略和 TOCTOU 防护。路径规范化本身不是针对目录遍历或符号链接竞争的授权检查。
from pathlib import Path
candidate = (Path("uploads") / "incoming.txt").resolve(strict=False)
resolve() 是路径计算和链接解析工具,不是权限授予;字符串前缀比较还会把 uploads-other 误判成 uploads。在检查后再按路径打开文件时,攻击者可替换目录项或链接,因此高风险场景需要平台提供的受控打开与授权机制。
资源清理与文件错误
文件流、临时资源和目录操作可能抛出保留原因的 OSError 子类;创建资源的一方负责在成功和异常路径都关闭或清理。用 with 缩小资源生命周期,并按需要处理具体异常;诊断信息仍应保留路径、操作和原始错误。
from pathlib import Path
try:
Path("lesson.txt").read_text(encoding="utf-8")
except FileNotFoundError:
text = "missing"
不要用宽泛的 except OSError: pass 掩盖权限、磁盘空间或并发删除问题,也不要依赖垃圾回收及时关闭文件。清理失败也应按资源所有权记录和处理,而不是假设所有平台都会立即释放句柄。