文件路径与文件系统操作
路径只是位置的描述,不代表文件一定存在。在常规应用开发中,优先使用 pathlib.Path,它将路径构建与文件系统操作统一在一个跨平台接口中,比拼接字符串更可靠。
from pathlib import Path
root = Path("data")
report = root / "reports" / "summary.csv"
if report.is_file():
size = report.stat().st_size
相对路径基于进程的当前工作目录(CWD)解析。CWD 由调用方决定,可能与脚本所在目录不同。务必将基准目录作为显式的配置项或参数传入,不要依赖隐式的环境状态。
如果资源文件刻意放置在独立脚本旁边:
script_dir = Path(__file__).resolve().parent
template = script_dir / "templates" / "report.txt"
对于已安装包内的数据,请使用 importlib.resources。安装包可能以 zip 形式存在,而非普通的相邻文件目录结构。
展开与解析
configured = Path("~/exports").expanduser()
absolute = configured.resolve()
expanduser() 负责处理 ~ 主目录标记。resolve() 则将路径转为绝对路径、规范化路径并解析符号链接。两者语义不同,切勿混用或视为简单的字符串清洗。
严禁使用字符串前缀匹配来验证路径包含关系。例如:/safe-backup 以 /safe 开头,但它并不在 /safe 目录内。正确做法是解析预期的根目录和目标路径,然后使用 relative_to 等路径关系进行判断。即便如此,这仅是安全边界的一环,因为在检查路径和使用路径之间,符号链接或文件系统状态可能发生变化(TOCTOU 竞态)。
文件发现
markdown_files = sorted(root.rglob("*.md"))
Glob 返回结果的顺序不具备跨平台一致性。若处理逻辑依赖确定性顺序,必须显式排序。目录遍历过程中可能遇到权限拒绝、断链、因跟随链接导致的循环,以及扫描期间被删除的文件。
文件操作
Path.mkdir、rename、replace、unlink 和 rmdir 覆盖常见的单路径操作。涉及目录树的复制、移动或删除时,使用 shutil.copy2、copytree、move 和 rmtree。这些函数在元数据保留和跨文件系统行为上存在差异,不要假设重命名或复制是原子操作,需查阅具体文档确认其行为契约。
执行删除或覆盖操作前,请遵循以下安全准则:
- 解析并打印出确切的目标路径。
- 验证目标路径位于预期的窄根目录之下。
- 拒绝空路径、文件系统根目录、用户主目录或工作区根目录。
- 在可行时,优先使用可恢复的移动或备份策略。
- 若其他进程可能修改目标,需预期“检查时/使用时”(TOCTOU)竞态条件。
“先检查存在性,再执行操作”并不构成保证。应直接执行操作并捕获具体异常。除非 OSError 的所有潜在原因确实共享同一套恢复策略,否则避免笼统地捕获 OSError。
构造路径不等于校验路径
pathlib 契约允许绝对路径的右操作数替换前面的路径。Path("") 表示当前目录,因此应在构造 Path 之前拒绝空的原始输入。Path 使用当前操作系统的路径规则;只需分析另一平台的语法而不访问文件系统时,可用 PureWindowsPath 或 PurePosixPath。
from pathlib import Path, PurePosixPath
from tempfile import TemporaryDirectory
assert PurePosixPath("/safe") / "/other" == PurePosixPath("/other")
assert Path("") == Path(".")
with TemporaryDirectory() as directory:
root = Path(directory)
reports = root / "reports"
reports.mkdir(parents=True, exist_ok=True)
draft = reports / "draft.txt"
draft.write_text("ready", encoding="utf-8")
final = draft.replace(reports / "final.txt")
assert final.name == "final.txt"
assert final.stem == "final" and final.suffix == ".txt"
assert final.read_text(encoding="utf-8") == "ready"
final.unlink()
reports.rmdir()
resolve() 默认使用 strict=False:尽可能解析已有部分,返回的路径仍可能包含不存在的后续部分。如果缺失部分必须触发 FileNotFoundError,使用 strict=True,但这仍不能保证路径随后一直存在。某些交互环境中没有 __file__。
即使有 exist_ok=True,mkdir 遇到同名非目录对象也会失败。rmdir() 要求目录为空;unlink() 删除文件或符号链接本身,不删除链接目标。replace() 可以覆盖已有文件;目标存在时,rename() 的行为因平台而异。示例把这些操作限制在自己的临时目录内。