Python批量重命名几万文件:从pathlib脚本到安全可恢复的完整流程
2026/9/4 15:14:48 网站建设 项目流程

处理几万个文件,还要用 Python 批量重命名,大多数人第一反应是“写好脚本,运行一次,完事”。我以前也是这么想的,直到真的面对一个目录下两万多个日志文件时才发现,真正难住人的不是“怎么写 rename”,而是“怎么保证批量改完以后,文件没丢、没重、没乱、还能找回原来的关系”。

如果你处理的文件只有几十个,那手工改也没什么大问题。但一旦到“几万个文件”这个量级,批量重命名就不再是一个语法题,而是一个流程题。代码负责执行“旧名字变成新名字”这个动作,但你必须先想清楚:命名规则是什么?排序按什么来?目标文件名会不会撞车?改到一半失败了怎么办?有没有一份能回滚的对照清单?

这篇文章会用 Python 给出适合批量重命名的处理思路,从最朴素的pathlib遍历开始,到处理重名、自然排序、Excel 清单映射,再到日志和断点续跑。你不需要把代码全部背下来,更重要的是理解一条主线:批量重命名不是“越改越快”,而是“改之前可计划,改之中可观察,改之后可恢复”。

1. 先想清楚:批量重命名的真正难点到底在哪

1.1 单次修改很容易,批量操作难在“可恢复”

把单个文件从old_name.txt改成new_name.txt,任何脚本都能做到。真正的问题出现在“批量”之后:

  • 你有几万个文件,无法靠肉眼逐个确认。
  • 排序规则不明确时,新名字可能和想象中完全对不上。
  • 如果两个文件的旧路径指向同一个新路径,后执行的脚本可能覆盖前一个文件。
  • 如果文件正在被 Word、Excel 或其他程序占用,重命名会中途报错。
  • 如果脚本跑到一半服务器断电、进程被杀,你已经不知道哪些文件改过、哪些没有。

所以,批量重命名真正难的不是“会不会写代码”,而是“怎么写才能让这个操作可以恢复、可以验证、可以重跑”。

这里说的“可恢复”,不一定强制让你把所有文件先复制一份。几万个文件如果全部复制,会占用大量磁盘和时间。更务实的做法是保留一份“旧文件名到新文件名的映射表”:有了旧名和新名的对应关系,即使执行出问题,也能写一个反向脚本把名字改回去。前提是文件内容没有被覆盖,文件名冲突在事前已经解决。

1.2 面向几万个文件,先建立一个“最小闭环”

我处理大批量文件时,会先建立这样一个最小闭环:

  1. 列出当前所有文件名,导出一份完整清单。
  2. 根据业务规则生成新文件名。
  3. 检查新文件名是否有重复、是否有目标已存在。
  4. 先做“只打印,不改名”的演练。
  5. 在小范围样本上执行并通过校验。
  6. 分批正式执行,同时记录日志。
  7. 结束后抽样检查文件内容,保留映射表。

很多人会跳过前四步,直接写一个“看起来正确”的脚本,跑完才发现编号顺序不对,或者覆盖了一批不想覆盖的文件。如果你想用 Python 批量处理几万个文件,最值得养成的习惯,就是永远先让脚本“只说不做”。

2. 环境准备:其实不用装一堆第三方库

2.1 确认当前 Python 环境够用

大多数批量重命名任务,只需要 Python 标准库,不需要额外下载几十个依赖包。处理路径和文件遍历,优先用pathlibos

在开始之前,先确认你的环境中有可用的 Python:

python --version

如果提示python不存在,可以试试:

python3 --version

如果你的机器上还没装 Python,那就去官网下载对应系统版本,安装时勾选“Add Python to PATH”之类的选项。安装完成后再用上面的命令验证。不同系统的安装细节略有差别,但最终目的都是让终端里能运行python

2.2 用 pathlib 处理路径,而不是手动拼字符串

处理文件名时,最容易出错的点不是改名本身,而是路径拼接。

很多初学会这样写:

file_path = folder + "/" + file_name

这在 Linux 上没问题,但在 Windows 上可能因为分隔符问题出错。更稳妥的方式是用pathlib.Path

from pathlib import Path root = Path("D:/data/files") for file in root.iterdir(): if file.is_file(): print(file.name)

Path对象会自动适配当前系统的路径分隔符,而且它提供了.name.suffix.stem.with_name()等方法,比手动操作字符串更直观。

2.3 涉及 Excel 映射时,再引入扩展库

如果你只是给文件加序号、加前缀、去空格,用标准库就够了。如果需求来自一张 Excel 表,需要根据 Excel 中的新旧文件名对应关系来重命名,那通常可以用pandas读取 Excel,再用标准库完成改名。

安装这一步可以用:

pip install pandas openpyxl

pandas负责读取表格,openpyxl负责解析.xlsx文件。如果网络安装慢,可以配置国内镜像源,但这属于环境问题,不会影响脚本本身的逻辑。

3. 第一个可运行脚本:先打印,再改名

3.1 dry_run:让脚本“只说不做”

假设你有一个目录,里面有很多.txt文件,你想按照它们当前的文件名排序,改名为report_0001.txtreport_0002.txt这样带序号的新名字。

第一步不是急着rename,而是先写一个 dry_run,把所有“旧名字 -> 新名字”打印出来。

from pathlib import Path root = Path("./test_files") files = [p for p in root.iterdir() if p.is_file() and p.suffix == ".txt"] files.sort(key=lambda p: p.name) for index, old_path in enumerate(files, start=1): new_name = f"report_{index:04d}{old_path.suffix}" new_path = old_path.with_name(new_name) print(f"{old_path.name} -> {new_path.name}")

这里old_path.with_name(new_name)的意思是:保留原文件所在的目录,只替换文件名本身。比如原路径是/data/raw/abc.txt,新文件路径就是/data/raw/report_0001.txt

:04d的作用是补零到 4 位:如果 index 是 7,输出就是0007。这样生成的文件名在资源管理器里排序时不会出现1、10、100挤在一起的乱象。

3.2 确认清单后,再真正执行 rename

你打印出来的清单如果符合预期,再进行第二步。

rename_plan = [] for index, old_path in enumerate(files, start=1): new_name = f"report_{index:04d}{old_path.suffix}" new_path = old_path.with_name(new_name) rename_plan.append((old_path, new_path)) for old_path, new_path in rename_plan: if new_path.exists(): print("跳过,目标已存在:", new_path) continue old_path.rename(new_path) print(f"完成:{old_path.name} -> {new_path.name}")

这里有一个容易被忽略的关键点:new_path.exists()检查不能省略。虽然你是根据原文件名生成的新文件名,正常情况它不应该存在,但真实目录里可能出现意外。

如果你不做检查,在某些系统上rename()会直接覆盖目标文件。一旦把本来存在的文件覆盖掉,后续很难恢复。

3.3 把计划写入文件,而不是只在屏幕上看

几万个文件的改名计划,不能只靠控制台输出。建议把计划也写到一个文本文件里,方便后续对账和回滚。

with open("rename_plan.tsv", "w", encoding="utf-8") as f: for old_path, new_path in rename_plan: f.write(f"{old_path}\t{new_path}\n")

这样一来,你有一份可以用 Excel 打开的清单。如果最终执行完毕,你还想验证文件是否改对,可以对照这份文件逐条抽查。

4. 大量文件重命名的几个隐藏大坑

4.1 自然排序:不要把 1、10、2 当成 1、2、10

当你想给文件加序号时,依赖排序规则非常重要。

看这段代码:

files.sort(key=lambda p: p.name)

如果目录里的文件名是file1.txtfile2.txtfile10.txt,字符串排序会得到:

file1.txt file10.txt file2.txt

因为字符串比较是按字符逐个来的:file10的第三位是1,而file2的第三位是2,所以file10会排在file2前面。

如果你希望按人类认知中的数字顺序排序,需要先提取文件名的数字部分,转成整数后再排序。可以这样实现一个“自然排序”的函数:

import re def natural_key(name): parts = re.split(r"(\d+)", name) return [int(part) if part.isdigit() else part.lower() for part in parts] files.sort(key=lambda p: natural_key(p.name))

这里的思路是把file2.txt拆成["file", 2, ".txt"],把数字部分转成int,这样排序时2 < 10就成立。

4.2 重名与覆盖:文件丢失往往来自这一行

批量重命名最危险的情况就是“目标文件已经存在”,但脚本没有发现。

举个例子:

  • a.txt要改成b.txt
  • 目录里本来已经有一个b.txt
  • 如果a.txt.rename("b.txt")在某个系统上允许覆盖,那原来的b.txt内容会被a.txt覆盖掉。

你还会遇到另一种情况:两个不同的旧文件要改成同一个新名字。比如:

new_names = ["data.txt", "data.txt"]

这是规则错误,应该在执行前就发现。检查重复可以用一个简单的集合:

from collections import Counter name_counts = Counter(new_names) duplicates = {name for name, count in name_counts.items() if count > 1} print("重复的新文件名:", duplicates)

如果 duplicates 不为空,就不要执行批量改名。

4.3 特殊字符、路径长度、文件占用和跨平台差异

批量处理时,还需要注意一些看起来不起眼,但会真实卡住流程的小问题:

  • Windows 系统不允许文件名包含\ / : * ? " < > |,如果新名字里有这些字符,会报OSError
  • Linux 区分大小写,所以a.txtA.txt可以同时存在;Windows 不区分大小写,所以这种改名可能会冲突。
  • 单个文件路径如果过长,在 Windows 上可能触发路径长度限制。
  • 文件如果正被 Excel、Word、进程或编辑器打开,重命名时会报PermissionError

在实际项目中,我会把需要转换的文件先做一次“黑名单”检查。如果发现新文件名包含当前系统不允许的字符,就提前打印出来,而不是等脚本跑一半才报错。

5. 用 Excel 清单驱动重命名,把规则放到外部

5.1 为什么要用外部清单管理

当文件数量少、规则也很简单时,直接在脚本里写映射关系没有问题。但有几万个文件时,业务规则往往很复杂:可能要根据设备编号、日期、项目代码重新组织名字,也可能需要把一个 Excel 表格中的“旧文件名”列和“新文件名”列关联起来。

如果这些规则只存在于代码里,后续维护的人很难看懂:为什么这个文件要改成这个名字?如果规则在 Excel 里,业务人员可以核对,开发人员只需要执行表格中的映射,职责更清楚。

5.2 用 pandas 读取 Excel 映射

假设 Excel 文件叫做rename_map.xlsx,里面有两列:old_namenew_name

读取时可以这样:

import pandas as pd df = pd.read_excel( "rename_map.xlsx", dtype={"old_name": "str", "new_name": "str"} ) mapping = dict(zip(df["old_name"], df["new_name"]))

这里dtype很重要。如果你让 pandas 自动推断类型,像001.xlsx这种文件名可能会被读成数字 1,丢掉前导零,导致后续找不到文件。把它强制指定为字符串,能少踩很多坑。

读出来后,先做基础清洗:

cleaned_mapping = {} for old, new in mapping.items(): old = str(old).strip() new = str(new).strip() if not old or not new: continue cleaned_mapping[old] = new

5.3 执行前先检查缺失、重复和冲突

拿到映射关系以后,你不能直接遍历执行,先要过一遍检查。

from pathlib import Path base_dir = Path("./files") missing_files = [] conflict_files = [] for old, new in cleaned_mapping.items(): src = base_dir / old dst = base_dir / new if not src.exists(): missing_files.append(old) if dst.exists() and src.resolve() != dst.resolve(): conflict_files.append((old, new)) print("源文件缺失:", len(missing_files)) print("目标已存在:", len(conflict_files))

为什么要检查src.resolve() != dst.resolve()?因为如果旧文件路径和新文件路径实际指向同一个文件,即使目标路径“存在”,也不是冲突,因为你本来就是想把它改成自己。如果目标路径存在但指向的是另一个文件,那才是真正的覆盖风险。

等检查通过后,再执行正式改名。

for old, new in cleaned_mapping.items(): src = base_dir / old dst = base_dir / new if not src.exists(): continue if dst.exists() and src.resolve() != dst.resolve(): continue src.rename(dst)

这种写法虽然不如“几行搞定”看起来简洁,但它把错误留在执行前解决,而不是让错误在几万个文件中随机爆发。

6. “两两交换文件名”这类特殊场景怎么处理

6.1 直接改名遇到“目标已存在”怎么办

普通批量重命名,最怕的不是“旧文件找不到”,而是“目标文件已经有了,需要先腾位置”。

比如你有一个业务规则:

  • a.txt改成b.txt
  • b.txt改成c.txt

如果按顺序执行第一条,系统发现目标b.txt已经存在,可能会报错或覆盖。即使你提前把b.txt改成了c.txt,再执行第一条,第二条又会找不到原来的b.txt

这就是“改 A 时需要 B 的位置,但 B 本身也要改”的连锁问题。

6.2 用两步临时改名法打破循环

处理这种场景,最稳妥的方法是两步走:

第一步,把所有“即将被改名的旧文件”先改成临时文件名。临时文件名要保证不会和任何新名、旧名冲突,可以用带有随机串的隐藏文件。

import uuid from pathlib import Path base_dir = Path("./test_files") pairs = [ ("a.txt", "b.txt"), ("b.txt", "c.txt"), ] temp_records = [] # 第一步:统一改成临时名 for old, _ in pairs: src = base_dir / old if not src.exists(): continue tmp_name = f".__renaming_{uuid.uuid4().hex}.tmp" tmp_path = src.with_name(tmp_name) src.rename(tmp_path) temp_records.append((tmp_path, old)) # 第二步:临时名改成最终新名 for tmp_path, old in temp_records: new = dict(pairs)[old] dst = base_dir / new tmp_path.rename(dst)

这样做有一个明显的好处:在整个改名过程中,不会出现“某个中途目标名被占用”的情况。所有文件先被挪到临时位置,相当于把旧空间清空,然后再按最终目标名填回去。

如果中途出现异常,你还有一份“原文件名 -> 临时文件名”的记录。通过 reverse 脚本,你可以把临时名再改回旧名,恢复到执行前状态。这种方式比直接覆盖安全得多。

7. 几万文件落地经验:分批、日志与断点续跑

7.1 一次跑完和分批跑,差异在哪里

几万个本地文件改名,通常不会真的运行几个小时。rename大多只是修改文件系统的元数据,速度并不慢。真正不建议一次跑完的原因,是“一旦出错,排查范围太大”。

比如你批量跑了两万个文件,在第五千个时报错了。这时候你需要知道:前五千个已经改完,后面一万五没处理。如果没有日志,你只能靠遍历目录去猜。

所以对几万个文件,我更建议按目录、按前缀或者按固定数量分批执行。例如一次只处理前 1000 个,确认结果正常后,再处理下一批。

7.2 日志不只是打印,还要持久化到文件

控制台的 print 信息,在窗口关闭后就没有了。要支持长期运行和错误排查,应该把处理结果写到一个日志文件里。

可以用最简单的 TSV 格式:

with open("rename.log", "a", encoding="utf-8") as log: for old_path, new_path in rename_plan: try: old_path.rename(new_path) log.write(f"{old_path}\t{new_path}\tOK\n") except Exception as exc: log.write(f"{old_path}\t{new_path}\tERROR\t{exc}\n")

这样做的好处是,当你想知道某个文件是否已经改过,可以搜索日志里的旧路径;当你想知道哪些文件失败,可以过滤ERROR这一列。

7.3 断点续跑:如何避免二次处理同一批文件

如果脚本中途异常,下一次运行时不能把所有文件从头再跑一遍。因为你已经改过的那些文件,可能已经不在旧路径下了。

一个简单策略是:在开始执行前,先读取日志中已经成功的旧路径,构造一个“已完成集合”。

done = set() with open("rename.log", "r", encoding="utf-8") as log: for line in log: parts = line.rstrip("\n").split("\t") if len(parts) >= 3 and parts[2] == "OK": done.add(parts[0]) for old_path, new_path in rename_plan: if str(old_path) in done: continue old_path.rename(new_path)

日志中的第一列是旧路径。如果该路径已经成功处理过,下次启动就跳过。

对于第一次接触这套思路的人,我的建议不是一上来就写复杂的断点续跑,而是先保证日志里能看到“每一条计划对应的执行结果”。有了结果,续跑就只是一个查集合的问题。

8. 报错排查链路和预防清单

8.1 最常遇到的几个异常

真实运行中,这几个异常出现频率最高:

异常信息含义常见原因
FileNotFoundError找不到源文件路径拼接错误、文件已经被前面步骤改走、扩展名不对
PermissionError没有权限文件被其他程序打开、目录只读、当前账户权限不足
FileExistsError目标已存在目标文件已经存在,且当前环境不允许覆盖
OSError文件系统错误新文件名含非法字符、路径过长、磁盘断开

8.2 从现象出发的排查顺序

如果你执行的脚本报错,不要马上来回改参数。我一般会按这个顺序排查:

第一步,先看是“全部都没改”还是“改到某一条开始报错”。如果是全部都没改,大概率是目录路径、权限或者文件列表为空的问题。如果改到某一条才报错,大概率是那条文件特有的问题。

第二步,把改名动作注释掉,重新打印计划。看看那一条的旧路径是否真实存在,新路径是否有非法字符。

第三步,检查脚本里是否用的是Path,还是手工拼接字符串。手工拼接很容易出现/data/files /a.txt这种多一个空格或者少一个斜杠的问题。

第四步,如果只有个别文件报PermissionError,先确认文件没有被 Word、Excel、文本编辑器或后台进程占用。Windows 下尤其容易出现文件被占用后无法改名的现象。

第五步,实在找不到原因时,复制一个单文件到测试目录,用同样的规则手动执行一次。最小样本能把问题从“批量环境”中剥离出来。

8.3 我建议的“重命名前中后检查清单”

把下面这份清单保留下来,至少能帮你避免大多数低级事故。

重命名前:

  • 导出现有文件名清单,确认文件总数。
  • 明确新文件名规则,生成完整映射表。
  • 检查目标文件名是否重复。
  • 检查目标文件是否已经存在。
  • 确认排序规则,而不是依赖目录遍历顺序。
  • 先运行 dry_run,打印前 20 条、后 20 条人工核对。

执行中:

  • 优先分批执行,不建议一次性处理全部文件。
  • 每处理一条都写日志,记录旧路径、新路径、执行状态。
  • 每跑完一批,检查已处理数量是否等于预计数量。

执行后:

  • 抽查若干文件内容,确认内容没有被覆盖。
  • 对比目录文件总数,确认没有丢失。
  • 保留日志和映射表,至少保留到业务确认无误之后。

回到最开始的问题:几万个文件怎么快速重命名?用 Python 做这件事,语法层面半天就能掌握。真正让你从“能跑”走向“能稳定交付”的,永远是那套可验证、可记录、可恢复的流程。文件越多,越不能靠“赌一把能成功”的兴奋感,而要靠执行前的检查和执行中的日志。把这一步想清楚,批量重命名对你来说就不再是高风险操作。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询