一、为什么 pathlib 优于 os.path 传统代码经常这样拼接路径:
1 2 import os path = os.path.join("data" , "reports" , "today.csv" )
pathlib 将路径封装成 Path 对象:
1 2 from pathlib import Path path = Path("data" ) / "reports" / "today.csv"
它的优势包括:
/ 运算符拼接路径,避免手写分隔符;
path.exists()、path.is_file()、path.stat() 语义清晰;
path.read_text()、path.write_text() 适合小型文本文件;
glob()、rglob()、iterdir() 可以直接遍历;
同一套代码可运行于 Linux、macOS 和 Windows。
Path 适合真实文件系统;PurePath 只进行路径计算,不访问磁盘,适合测试和解析外部输入。
二、核心 API iterdir() 遍历当前目录,glob("*.log") 匹配当前层,rglob("*.log") 递归匹配。生产脚本要明确是否跟随符号链接,避免目录循环。
1 2 3 4 5 6 from pathlib import Path root = Path("data" )for file in root.rglob("*.log" ): if file.is_file(): print (file, file.stat().st_size)
对于大量文件,避免先转换成巨大的列表。使用迭代器可以降低内存峰值。删除和移动操作则必须增加 dry-run 预览,尤其是清理脚本。
三、实战 1:批量重命名 下面脚本把目录中的素材统一命名为 project_0001.ext。它先生成计划,再执行,且通过临时名称避免重命名互相覆盖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from pathlib import Path ROOT = Path("photos" ) PREFIX = "project" DRY_RUN = True files = sorted ( p for p in ROOT.iterdir() if p.is_file() and p.suffix.lower() in {".jpg" , ".jpeg" , ".png" } ) plan = [ (src, ROOT / f"{PREFIX} _{i:04d} {src.suffix.lower()} " ) for i, src in enumerate (files, 1 ) ]for src, dst in plan: print (f"{src.name} -> {dst.name} " )if not DRY_RUN: temp_plan = [] for index, (src, dst) in enumerate (plan): temp = ROOT / f".rename_tmp_{index} " src.rename(temp) temp_plan.append((temp, dst)) for temp, dst in temp_plan: if dst.exists(): raise FileExistsError(dst) temp.rename(dst)
重命名前不要依赖文件系统返回顺序。sorted() 能让结果可复现,也便于回滚和审计。
四、实战 2:按 EXIF 时间归档照片 照片可以通过 EXIF 拍摄时间归档到 年/月/日。生产环境中要考虑没有 EXIF 的文件,脚本可以回退到文件修改时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 from datetime import datetimefrom pathlib import Pathimport shutiltry : from PIL import Image from PIL.ExifTags import TAGSexcept ImportError: raise SystemExit("请先执行: python -m pip install pillow" ) SOURCE = Path("incoming" ) DEST = Path("archive" ) DRY_RUN = True def photo_time (path: Path ) -> datetime: try : with Image.open (path) as image: exif = image.getexif() for key, value in exif.items(): if TAGS.get(key) == "DateTimeOriginal" : return datetime.strptime(str (value), "%Y:%m:%d %H:%M:%S" ) except Exception: pass return datetime.fromtimestamp(path.stat().st_mtime)for photo in SOURCE.rglob("*" ): if not photo.is_file() or photo.suffix.lower() not in {".jpg" , ".jpeg" , ".png" }: continue when = photo_time(photo) target_dir = DEST / f"{when:%Y} " / f"{when:%m} " / f"{when:%d} " target = target_dir / photo.name if target.exists(): target = target_dir / f"{photo.stem} _{photo.stat().st_size} {photo.suffix} " print (f"{photo} -> {target} " ) if not DRY_RUN: target_dir.mkdir(parents=True , exist_ok=True ) shutil.move(str (photo), str (target))
归档脚本应保留原始目录结构或生成清单,否则同名文件和后续追溯都会困难。
五、实战 3:mtime 增量备份 最简单的增量策略是比较源文件和目标文件的修改时间及大小。更严格的备份可以比较 SHA-256,但会增加磁盘读取量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pathlib import Pathimport shutil SOURCE = Path("project" ) BACKUP = Path("backup/project" ) DRY_RUN = False for src in SOURCE.rglob("*" ): if not src.is_file(): continue relative = src.relative_to(SOURCE) dst = BACKUP / relative changed = ( not dst.exists() or src.stat().st_size != dst.stat().st_size or src.stat().st_mtime_ns > dst.stat().st_mtime_ns ) if changed: print ("backup:" , relative) if not DRY_RUN: dst.parent.mkdir(parents=True , exist_ok=True ) shutil.copy2(src, dst)
copy2() 会尽量保留修改时间等元数据。备份到对象存储时,可使用 rclone、云厂商 CLI 或 SDK;不要把密钥硬编码进脚本。
六、实战 4:清理旧日志 清理任务必须限定目录、限定后缀,并先打印待删除文件:
1 2 3 4 5 6 7 8 9 10 11 12 from datetime import datetime, timedeltafrom pathlib import Path LOG_DIR = Path("/var/log/myapp" ) DAYS = 7 cutoff = datetime.now().timestamp() - timedelta(days=DAYS).total_seconds()for path in LOG_DIR.glob("*.log" ): if path.is_file() and path.stat().st_mtime < cutoff: print ("remove:" , path)
不要直接对用户传入的路径执行 rmtree()。shutil.rmtree() 只能用于明确创建的临时目录,并且应检查其是否位于允许的根目录之下。
七、shutil 高级用法 copytree(src, dst, dirs_exist_ok=True) 可合并复制目录;ignore 参数可排除缓存和构建产物:
1 2 3 4 5 6 7 8 9 10 11 12 import shutilfrom pathlib import Pathdef ignore_build (directory: str , names: list [str ] ) -> set [str ]: return {name for name in names if name in {".git" , "__pycache__" , "node_modules" }} shutil.copytree( Path("src" ), Path("backup/src" ), dirs_exist_ok=True , ignore=ignore_build, )
move() 在同一文件系统中通常是重命名,跨文件系统时可能退化为复制后删除。备份关键数据时应验证目标文件大小或哈希。
八、踩坑记录与跨平台测试 Windows 路径可能超过传统 260 字符限制,尽量缩短目录层级,并使用较新的 Windows API。符号链接可能造成 rglob() 循环,处理链接时检查 path.is_symlink()。Unicode 文件名在 Linux 通常是 UTF-8,在 Windows 则要避免依赖系统默认编码。
测试时不要只在自己的机器上运行。可以使用 PurePosixPath 和 PureWindowsPath 验证路径计算:
1 2 3 4 from pathlib import PurePosixPath, PureWindowsPathassert str (PurePosixPath("a" ) / "b" ) == "a/b" assert str (PureWindowsPath("a" ) / "b" ) == r"a\b"
九、小结
任务
推荐 API
路径拼接
Path / "name"
递归查找
rglob()
文件复制
shutil.copy2()
目录复制
shutil.copytree()
安全删除
unlink()、受控 rmtree()
跨平台路径计算
PurePath
无论是重命名、归档还是备份,都应遵循“预览、校验、执行、记录”四步流程。