1. 批量重命名文件并生成映射日志
任务拆解:遍历 → 规则改名 → 写映射日志
本案例要完成三件事:用 pathlib 遍历目标目录中的文件;按统一规则生成新文件名并执行重命名;把「旧名 → 新名」逐行写入日志文件,使任何一次批量操作都能被反向还原。核心难点不在改名本身,而在于冲突检测与可回滚:如果新名已存在,直接覆盖会造成数据丢失;如果没有日志,回滚就无从下手。
命名规则与冲突处理约定
示例采用一条简单且可验证的规则:把文件名统一为 前缀_序号.扩展名,例如 IMG_0421.JPG 变为 photo_0001.JPG。序号按文件名排序后从 1 开始递增,扩展名保持原样(含大小写)。冲突处理策略如下:
- 目标名已存在于目录中 → 跳过该文件,并在日志中标记
SKIP。 - 源文件与目标名相同 → 视为无需处理,标记
SKIP。 - 正常改名 → 标记
OK,并记录旧名与新名。
日志采用
旧名\t新名\t状态 的制表符分隔格式,既方便人阅读,也方便后续用脚本解析回滚。关键实现代码
下面脚本可直接运行。它先收集文件、排序、计算新名,再统一执行重命名并写日志,避免边遍历边改名导致的顺序混乱。
from pathlib import Path
TARGET_DIR = Path("./demo_files") # 待处理目录
PREFIX = "photo" # 新文件名前缀
LOG_FILE = Path("./rename_log.tsv") # 映射日志
def build_plan(files, prefix):
"""按排序结果生成 (旧路径, 新路径) 计划,并检测冲突。"""
plan = []
for index, old_path in enumerate(files, start=1):
new_name = f"{prefix}_{index:04d}{old_path.suffix}"
new_path = old_path.with_name(new_name)
plan.append((old_path, new_path))
return plan
def execute(plan, log_path):
"""执行重命名并写日志,返回统计结果。"""
ok = skip = 0
with log_path.open("w", encoding="utf-8") as log:
log.write("old_name\tnew_name\tstatus\n")
for old_path, new_path in plan:
if old_path == new_path or new_path.exists():
log.write(f"{old_path.name}\t{new_path.name}\tSKIP\n")
skip += 1
continue
old_path.rename(new_path)
log.write(f"{old_path.name}\t{new_path.name}\tOK\n")
ok += 1
return ok, skip
if __name__ == "__main__":
files = sorted(p for p in TARGET_DIR.iterdir() if p.is_file())
plan = build_plan(files, PREFIX)
ok, skip = execute(plan, LOG_FILE)
print(f"完成:成功 {ok} 个,跳过 {skip} 个,日志:{LOG_FILE}")
执行流程与日志结构
下图展示脚本从目录扫描到日志落盘的完整数据流,以及日志中三种状态字段的含义。注意 new_path.exists() 的检测发生在真正 rename 之前,这是防止覆盖的关键判断点。
运行与结果判读
准备一个测试目录,放入若干文件后运行脚本。示例目录内容与预期日志如下(以下为教学示例数据,非真实运行结果):
| 目录初始文件 | 排序后序号 | 新文件名 | 日志状态 |
|---|---|---|---|
| IMG_0421.JPG | 1 | photo_0001.JPG | OK |
| IMG_0422.JPG | 2 | photo_0002.JPG | OK |
| photo_0003.JPG(已存在) | 3 | photo_0003.JPG | SKIP |
| notes.txt | 4 | photo_0004.txt | OK |
判读要点:日志中 OK 行数应等于实际被改名的文件数;SKIP 行说明该文件未被动过,可安全忽略。若发现某个预期改名的文件出现在 SKIP 中,应检查是否与已有文件重名,或源文件本身已符合目标命名。
回滚操作
回滚只需读取日志中状态为 OK 的行,按「新名 → 旧名」反向执行 rename。由于日志记录了完整映射,即使目录中新增了其他文件,也不会误伤。
def rollback(log_path):
with log_path.open(encoding="utf-8") as log:
next(log) # 跳过表头
for line in log:
old_name, new_name, status = line.rstrip("\n").split("\t")
if status != "OK":
continue
new_path = TARGET_DIR / new_name
old_path = TARGET_DIR / old_name
if new_path.exists():
new_path.rename(old_path)
回滚前请确认目标目录中没有与旧名冲突的新文件,否则
rename 可能覆盖已有内容。生产环境中建议先做一次 dry-run 检查。可复用检查清单
- 运行前备份目录,或先在副本上验证规则。
- 确认
PREFIX与序号位数不会与现有文件冲突。 - 日志文件不要放在被扫描的目录内,否则可能被自身遍历到。
- 回滚前检查日志完整性,确保没有中途中断导致的半截记录。