Python pathlib批量改名并保留冲突回滚点的脚本
来源:17golang原创
时间:2026-09-20 08:38:24 422浏览 收藏
整理一批导入文件时,直接在循环里调用 Path.rename() 看起来很快,但目标名一旦重复,脚本可能执行到一半才失败,留下“改了一部分、又不知道改了哪些”的现场。更稳妥的做法是把改名拆成计划、校验、执行和回滚四个阶段:先用 pathlib 生成确定性映射,冲突时整个计划不落地,成功项再写入可逆清单。
这个脚本只处理指定目录的直接文件,不递归进入子目录;它会先生成rename-plan.json,发现目标已存在或目标重复就停止,执行成功后再生成rename-rollback.json。
先把源文件和目标文件变成一张计划表
Path 是面向当前操作系统的具体路径对象,适合同时完成目录遍历、名称拆分和重命名。计划表至少保留源路径、目标路径和是否已完成三个字段,后面检查冲突、写回滚清单都依赖它。
from pathlib import Path
import json
ROOT = Path("incoming")
def build_plan(root: Path) -> list[dict]:
# 只处理当前目录的普通文件,避免一次误改名整个目录树。
files = sorted((p for p in root.iterdir() if p.is_file()), key=lambda p: p.name)
plan = []
for index, source in enumerate(files, start=1):
# 保留原后缀,只改变主文件名,目标顺序由排序后的源文件决定。
target = source.with_name(f"asset-{index:03d}{source.suffix.lower()}")
plan.append({
"old": str(source),
"new": str(target),
"done": False,
})
return plan
plan = build_plan(ROOT)
Path("rename-plan.json").write_text(
json.dumps(plan, ensure_ascii=False, indent=2), encoding="utf-8"
)
这里先排序再编号,意味着同一批文件重复运行时计划是稳定的。suffix.lower() 只统一扩展名大小写;如果业务需要严格保留原始后缀,应改为直接使用 source.suffix,不要把“规范化”悄悄混进改名规则。

执行前同时拦住已存在目标和计划内重复
只检查 target.exists() 还不够:两个源文件也可能因为业务规则生成同一个目标名。将所有目标转成字符串集合后,分别检查“计划内重复”和“目录中已有目标”,任何一项命中都返回报告,不执行任何改名。
def find_conflicts(plan: list[dict]) -> list[str]:
# 先找多个源文件指向同一目标,再找目录中已有的目标文件。
conflicts = []
seen: dict[str, str] = {}
for item in plan:
old_path = Path(item["old"])
new_path = Path(item["new"])
key = str(new_path)
if key in seen:
conflicts.append(f"计划重复: {seen[key]} -> {key} 与 {old_path}")
else:
seen[key] = str(old_path)
# 源文件本身就是计划的一部分,不把它误判成外部冲突。
if new_path.exists() and new_path != old_path:
conflicts.append(f"目标已存在: {new_path}")
return conflicts
conflicts = find_conflicts(plan)
if conflicts:
for message in conflicts:
print(message)
raise SystemExit("计划存在冲突,未执行改名")
这个判断把“冲突”定义得很保守:即使目标是一个可以覆盖的临时文件,也先停下来让人确认。批处理脚本最怕默认覆盖,因为覆盖成功后往往无法分辨原文件属于哪一轮任务。
按计划执行,并记录每一条成功映射
冲突检查通过后,按计划表调用 rename()。每完成一条就把 done 写回计划文件,这样进程中断时可以知道最后完成的位置;同时只把已完成项放进回滚清单,避免回滚不存在的目标。
PLAN_FILE = Path("rename-plan.json")
ROLLBACK_FILE = Path("rename-rollback.json")
def save_json(path: Path, data: object) -> None:
# 统一 UTF-8 写入,便于人工复核中文文件名和恢复记录。
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
completed = []
for item in plan:
old_path = Path(item["old"])
new_path = Path(item["new"])
try:
# rename 保留同一文件系统内的原子改名语义,不复制文件内容。
old_path.rename(new_path)
except OSError as exc:
# 已完成项仍然保留,下一次可先回滚或人工处理权限问题。
save_json(ROLLBACK_FILE, list(reversed(completed)))
raise RuntimeError(f"改名失败: {old_path} -> {new_path}: {exc}") from exc
item["done"] = True
completed.append({"old": str(old_path), "new": str(new_path)})
save_json(PLAN_FILE, plan)
save_json(ROLLBACK_FILE, list(reversed(completed)))
清单使用“旧路径 → 新路径”的方向,回滚时则逆序执行“新路径 → 旧路径”。逆序的意义在于尽量先恢复后改名的文件,降低名称链条相互占用的概率;但它不是绕过外部文件占用的保证,回滚前仍要重新检查目标是否存在。

回滚函数要拒绝覆盖新文件
回滚不是把清单无条件反向执行。恢复前要确认新路径仍存在、旧路径没有被别的文件占用,否则宁可报告失败,也不要用恢复动作覆盖用户后来创建的文件。
def rollback(records: list[dict]) -> None:
for record in records:
current = Path(record["new"])
original = Path(record["old"])
# 旧名重新出现时说明目录状态已变化,禁止覆盖。
if not current.exists():
raise FileNotFoundError(f"回滚源不存在: {current}")
if original.exists():
raise FileExistsError(f"回滚目标已被占用: {original}")
current.rename(original)
# records = json.loads(ROLLBACK_FILE.read_text(encoding="utf-8"))
# rollback(records) # 仅在确认目录状态后手动调用
几个容易被忽略的边界
| 场景 | 处理建议 |
|---|---|
| 需要递归改名 | 先明确是否允许跨目录移动,再用 rglob() 并重新设计排序和冲突策略。 |
| 符号链接 | 默认不要跟随处理;把链接和真实文件分开列入计划,避免误改目标文件。 |
| 跨文件系统 | rename() 不等同于复制,跨设备时应改用复制、校验、删除的独立流程。 |
| 权限或文件被占用 | 保留已完成清单,修复权限后先确认现场,再决定回滚还是从剩余计划继续。 |
最终核对不应只看文件数量,还要读取清单逐条确认旧路径已经不存在、新路径确实存在。只要清单和目录状态不一致,就先停在人工处理状态,不要再次运行整个批次。
相关问题
为什么不直接用字符串拼接文件路径?
Path 会按运行平台处理路径分隔符,并把名称、后缀、存在性和改名操作放在同一套对象接口中,减少手写分隔符和扩展名切分的错误。
目标文件已存在时能不能覆盖?
除非覆盖本身就是明确需求,并且有独立备份,否则不建议。对批量整理脚本来说,停止并输出冲突清单比静默覆盖更容易恢复。
-
346 收藏
-
235 收藏
-
387 收藏
-
447 收藏
-
360 收藏
-
298 收藏
-
323 收藏
-
文章 · python教程 | 6小时前 | 并发 · 线程池 · 异常处理 · python · Python threadpoolexecutor future concurrent.futures262 收藏
-
文章 · python教程 | 7小时前 | 性能优化 · 缓存设计 · Python教程 · Python functools.lru_cache Python 可变参数缓存键 Python list dict 缓存 Python 缓存失效344 收藏
-
290 收藏
-
118 收藏
-
159 收藏
-
337 收藏
-
421 收藏
-
274 收藏
-
199 收藏
-
168 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习