ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python 批量改名与文件查重:先预览、再执行,并生成可回滚清单

Python 批量改名与文件查重:先预览、再执行,并生成可回滚清单 批量整理文件最容易犯的错误是把“计算新文件名”和“真正修改文件”写在同一个循环里。脚本运行到第 80 个文件时才遇到重名前 79 个已经被改掉这时既不知道完成了哪些也很难安全地重跑。更稳的做法是把任务拆成四步扫描目录、生成计划、人工预览、显式执行。执行后再保存一份反向映射作为回滚清单。下面的示例只检查指定目录的第一层普通文件不跟随符号链接不删除任何文件。它把IMG_20260102_030405.jpg转成2026-01-02_030405.jpg。第一步只生成改名计划from__future__importannotationsimportjsonimportrefromdataclassesimportasdict,dataclassfrompathlibimportPathfromtypingimportCallabledataclass(frozenTrue)classRename:source:strtarget:strdefphoto_name(name:str)-str:returnre.sub(r^IMG_(\d{4})(\d{2})(\d{2})_(\d{6})(\.[^.])$,r\1-\2-\3_\4\5,name,flagsre.IGNORECASE,)defbuild_plan(folder:Path,transform:Callable[[str],str])-list[Rename]:folderfolder.resolve(strictTrue)plan:list[Rename][]targets:set[Path]set()forsourceinsorted(folder.iterdir(),keylambdapath:path.name):ifsource.is_symlink()ornotsource.is_file():continuenew_nametransform(source.name)ifnew_namesource.name:continueifPath(new_name).name!new_nameornew_namein{.,..}:raiseValueError(f非法目标文件名{new_name!r})targetsource.with_name(new_name)iftarget.exists()ortargetintargets:raiseFileExistsError(f目标冲突{target.name})targets.add(target)plan.append(Rename(str(source),str(target)))returnplan这里先挡住三类常见问题目标文件已经存在两个源文件算出了同一个目标名转换函数返回了包含目录成分的名字。排序不是安全机制但能让每次预览更容易比较。预览时不要只看前几行。至少做五项核对计划数量是否等于预期所有源文件是否都在指定目录扩展名有没有被意外改变大小写不同的文件名在目标文件系统中会不会冲突没有匹配规则的文件是否确实保持不动。最好把 JSON 输出保存到本次任务的审计目录与执行时间、规则版本和备份快照放在一起。规则变化后重新生成计划不要继续执行旧清单。这里也故意只扫描第一层。递归扫描看起来更“自动”却会把目录边界、同名子目录和权限问题一起引入。确实需要递归时应该先定义允许进入的根目录、明确排除备份和隐藏目录并把相对路径加入计划。不要接受用户输入的任意绝对路径再让脚本自行遍历整块磁盘。第二步默认预览执行后写回滚清单defapply_plan(plan:list[Rename],manifest:Path,execute:boolFalse)-None:print(json.dumps([asdict(item)foriteminplan],ensure_asciiFalse,indent2))ifnotexecute:print(仅预览传入 executeTrue 才会执行。)returnforiteminplan:source,targetPath(item.source),Path(item.target)ifsource.is_symlink()ornotsource.is_file():raiseRuntimeError(f源文件状态已变化{source})iftarget.exists():raiseFileExistsError(f目标现在已存在{target})completed:list[Rename][]try:foriteminplan:source,targetPath(item.source),Path(item.target)source.rename(target)completed.append(item)finally:rollback[{source:item.target,target:item.source}foriteminreversed(completed)]manifest.write_text(json.dumps(rollback,ensure_asciiFalse,indent2),encodingutf-8,)rootPath(sample-files)planbuild_plan(root,photo_name)apply_plan(plan,root/rollback.json,executeFalse)第一次运行必须保留executeFalse先核对数量、扩展名和目标名称。确认备份可恢复后再显式改成True。执行前的第二次检查可以缩小扫描与执行之间的状态变化风险但不能彻底消除并发修改。rollback.json只记录已完成项目的反向映射它是审计和恢复输入不是“自动回滚保证”真正回滚也应先预览并重新检查冲突。恢复时建议把清单当作一份新的改名计划从最后完成的项目向前检查确认当前源路径仍然存在、原目标路径仍然空闲再决定是否执行。假如人工已经在新文件名下继续编辑直接回滚可能覆盖后续工作假如失败发生在跨文件系统移动中简单的反向rename也未必成立。因此示例没有提供“一键回滚”按钮更没有遇错后立即自动反向执行。保留现场、记录完成范围通常比在异常处理中继续批量修改更安全。执行窗口也需要管理。开始前暂停同步软件、照片导入器和其他会写入目录的任务执行后比较文件数量和总大小并随机打开若干文件验证可读性。文件名变了不代表内容损坏但只核对名称同样不足以证明整理成功。对重要资料可以在执行前后分别生成只读内容摘要再确认改名没有改变文件字节。第三步查重只生成报告不默认删除fromcollectionsimportdefaultdictfromhashlibimportfile_digestfrompathlibimportPathdefduplicate_report(folder:Path)-list[list[Path]]:by_size:dict[int,list[Path]]defaultdict(list)forpathinfolder.rglob(*):ifpath.is_symlink()ornotpath.is_file():continueby_size[path.stat().st_size].append(path)groups:list[list[Path]][]forcandidatesinby_size.values():iflen(candidates)2:continueby_hash:dict[str,list[Path]]defaultdict(list)forpathincandidates:withpath.open(rb)asstream:digestfile_digest(stream,sha256).hexdigest()by_hash[digest].append(path)groups.extend(itemsforitemsinby_hash.values()iflen(items)1)returngroups先按文件大小分组可以避免对明显不同的文件计算哈希SHA-256 只负责把相同字节内容放在一组并不证明它们在业务上可以互相替代。照片可能内容相同但元数据不同硬链接可能让多个路径指向同一份数据云盘占位文件也可能尚未完整下载。因此这段代码只输出报告没有任何删除分支。file_digest()需要 Python 3.11 或更高版本。查重结果真正需要的是保留策略而不是删除循环。可以为每组候选补充相对路径、大小、修改时间和是否硬链接等信息再把“保留哪一份”交给人工或一条可解释的业务规则。比如下载目录和已归档目录出现相同内容时保留归档版本可能合理两份照片字节相同但位于不同项目交付目录时它们仍可能都要保留。删除前还应再次计算摘要并确认文件状态没有改变。大目录还有读取一致性问题计算哈希期间文件若被其他进程修改前后结果可能对应不同状态。稳妥做法是在不可变快照、只读副本或暂停写入的窗口生成报告。对机械硬盘和网络存储还要控制并发读取避免为了“更快”导致磁盘随机寻道、网络拥塞或远端限流。优化之前先记录候选数量和实际读取量不能把多线程当作默认答案。还有几个边界需要提前写进验收清单。Path.rename()对已有目标的行为存在平台差异所以示例主动拒绝覆盖。shutil.copy2()即使复制成功也不保证完整保留属主、ACL 或 macOS 资源叉。文件修改时间不是照片拍摄时间如果以后使用 EXIF 归档还要单独处理位置隐私、时区缺失和元数据冲突。测试也要覆盖失败路径。除正常的两项改名外至少构造一个目标已存在、两个源映射到同一目标、一个符号链接和一个执行中断场景。通过标准不是“异常被捕获”而是冲突发生前没有改动或者中断后清单准确记录已完成范围。跨文件系统、云盘、网络挂载和真实照片元数据没有在当前验证环境中覆盖时应继续标记为未验证不能用本地临时目录的结果替代。这类脚本的完成标准不应该是“命令退出码为 0”而应是计划可预览冲突能提前发现中断后能看清已完成范围真实库存从未被当作测试目录。先用临时副本演练再对备份可恢复的目录执行比给删除逻辑增加更多条件更可靠。如果任务需要长期重复运行还应给计划文件加入规则版本和生成时间并禁止同一计划重复执行。自动化可以减少手工操作但必须让每一次变化都能被定位、复核和解释。参考资料Python pathlibhttps://docs.python.org/3/library/pathlib.htmlPython shutilhttps://docs.python.org/3/library/shutil.htmlPython hashlib.file_digesthttps://docs.python.org/3/library/hashlib.html#hashlib.file_digest
返回列表