news 2026/10/17 3:27:04

Python批量修改文件名实例操作分享

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python批量修改文件名实例操作分享

前言


批量改名听起来像"循环里调一下os.rename",真写起来会遇到三个立刻见血的问题:改成什么规则(加前缀、去空格、正则替换、统一编号)、改的时候会不会撞名、以及改了一半失败了怎么办。第三个问题最要命——改名是不可逆操作,脚本跑到第 50 个文件崩了,前 49 个已经改了,剩下的一片混乱,很难手工还原。


本文按"先想清楚命名规则 → 用 dry-run 预演 → 处理撞名 → 两阶段提交"的顺序,给出一份能安全跑在真实目录上的批量改名方案。全部基于标准库的pathlib与os。


先说一个 Windows 上特别容易中招的点:文件系统大小写不敏感。想把photo.jpg改成Photo.jpg,某些情况下这个操作会被当成"没有变化"而失败或静默无效。稳妥做法是两阶段改名:先改成一个临时名,再改成目标名。


一、确定改名规则


命名规则通常落在这几类:统一加前缀/后缀、去掉文件名里的空格或特殊字符、按正则替换片段、统一编号(img_001.jpg)。抽取"主名 + 扩展名"是关键一步,务必用Path.stem和Path.suffix,而不是自己split(".")——因为archive.tar.gz的主名是archive.tar,suffix是.gz,手工切很容易切错。


# 适用于 Python 3.8+
from pathlib import Path

p = Path("archive.tar.gz")
print(p.stem) # archive.tar
print(p.suffix) # .gz
print(p.suffixes) # ['.tar', '.gz']

改名时推荐用path.with_name(新文件名)来构造目标路径,它会保留父目录、只替换最后一段名字,比字符串拼接安全。也可以用with_stem()/with_suffix()单独换主名或后缀(这两个方法在Python 3.9+才提供)。


二、撞名检测与两阶段提交


改名最大的风险是"多个旧名映射到同一个新名"。比如把空格替换成下划线时,a b.txt和a_b.txt会变成同一个目标。检测方式是先把整批映射算出来,检查目标名是否重复、是否已存在,再动手。


# 适用于 Python 3.8+
from pathlib import Path


def build_plan(files, rule):
"""rule: 接收 Path,返回新文件名(不含目录)。返回 (src, dst) 列表。"""
plan = []
seen = {}
for f in files:
new_name = rule(f)
dst = f.with_name(new_name)
if new_name in seen:
raise ValueError(f"目标名冲突: {f.name} 与 {seen[new_name]} 都想叫 {new_name}")
if dst.exists() and dst.name != f.name:
raise ValueError(f"目标已存在: {dst}")
seen[new_name] = f.name
plan.append((f, dst))
return plan

真正的"提交"分两步走:第一轮把每个源文件改成一个唯一的临时名,第二轮再把临时名改成目标名。这样即使两批名字互相交叉(比如要把a.txt改成b.txt、b.txt改成a.txt),也不会中途撞车。


# 适用于 Python 3.8+
import os


def apply_plan(plan):
"""两阶段改名,先统一移到临时名,再统一落到目标名。"""
staged = []
for src, dst in plan:
tmp = src.with_name(f"__tmp__{os.getpid()}_{src.name}")
os.rename(src, tmp)
staged.append((tmp, dst))
for tmp, dst in staged:
os.rename(tmp, dst)

用os.getpid()拼进临时名,避免同一目录下并行跑两个实例时临时名互相覆盖。


三、完整实例:四类常见规则


# 适用于 Python 3.8+
import argparse
import os
import re
from pathlib import Path


def rule_prefix(p):
return "2024_" + p.name


def rule_strip_spaces(p):
return re.sub(r"\s+", "_", p.stem) + p.suffix


def rule_normalize(p):
"""把主名里的非字母数字统一替换为下划线。"""
stem = re.sub(r"[^0-9A-Za-z一-鿿]+", "_", p.stem)
return stem.strip("_") + p.suffix.lower()


def make_number_rule():
"""返回一个有状态的编号规则:每次调用递增,从 001 开始。"""
state = {"n": 0}

def rule(p):
state["n"] += 1
return f"item_{state['n']:03d}{p.suffix.lower()}"

return rule


def build_rules():
return {
"prefix": rule_prefix,
"spaces": rule_strip_spaces,
"normalize": rule_normalize,
"number": make_number_rule(),
}


def build_plan(files, rule):
plan, seen = [], {}
for f in files:
new_name = rule(f)
dst = f.with_name(new_name)
if new_name in seen:
raise ValueError(f"目标名冲突: {f.name} 与 {seen[new_name]} -> {new_name}")
if dst.exists() and dst.name != f.name:
raise ValueError(f"目标已存在: {dst}")
seen[new_name] = f.name
plan.append((f, dst))
return plan


def apply_plan(plan):
staged = []
for src, dst in plan:
tmp = src.with_name(f"__tmp__{os.getpid()}_{src.name}")
os.rename(src, tmp)
staged.append((tmp, dst))
for tmp, dst in staged:
os.rename(tmp, dst)


def main():
rules = build_rules()
ap = argparse.ArgumentParser(description="批量重命名文件")
ap.add_argument("root")
ap.add_argument("--rule", choices=sorted(rules), required=True)
ap.add_argument("--apply", action="store_true", help="真正执行,默认只预演")
args = ap.parse_args()

root = Path(args.root)
files = [f for f in root.iterdir() if f.is_file() and not f.name.startswith(".")]
files.sort(key=lambda p: p.name)
plan = build_plan(files, rules[args.rule])

for src, dst in plan:
print(f"{src.name} -> {dst.name}")
if args.apply:
apply_plan(plan)
print(f"已重命名 {len(plan)} 个文件")
else:
print("(预演模式,加 --apply 才会真正执行)")


if __name__ == "__main__":
main()

files的顺序依赖iterdir()的返回顺序。如果规则里用了编号,最好先files.sort(key=lambda p: p.name)固定顺序,否则"第几个文件变成 item_001"每次可能不一样。




需求写法注意



加前缀"2024_" + p.name直接拼name,别丢后缀

替换空格re.sub(r"\s+", "_", p.stem)只处理主名,后缀单独接回

统一小写后缀p.suffix.lower()后缀含点,别漏

按序号命名f"item_{i:03d}{p.suffix}"先排序保证顺序稳定

只改后缀p.with_suffix(".txt")3.9+ 有with_stem



常见坑点



  1. ❌ 用name.split(".")[0]取主名


✅a.tar.gz会被切成a,后缀全丢;用Path.stem/Path.suffix



  1. ❌ 边遍历边改名,导致重复处理或漏处理


✅ 先list(root.iterdir())固化成列表,算出完整映射再统一执行



  1. ❌ 直接os.rename而不管目标是否已存在


✅ 目标存在时 POSIX 上可能静默覆盖、Windows 上可能抛错,行为随平台漂移;提前做存在性检查



  1. ❌ 一批文件的新名字互相交叉(a→b、b→a)时直接一对一对改


✅ 用两阶段改名:先全部改成唯一临时名,再改成目标名



  1. ❌ 想把photo.jpg改成Photo.jpg却直接os.rename


✅ 大小写不敏感的文件系统上可能被当成无变化;先改临时名再改回来



  1. ❌ 没有预演,第一次就在真实目录上执行


✅ 默认 dry-run 打印映射表,确认后加--apply;重要目录先备份



  1. ❌ 正则替换只替换第一处,或把后缀也一起改了


✅re.sub默认替换全部;但要用stem参与匹配,最后再把suffix接回去



  1. ❌ 忘了跳过隐藏文件和临时文件


✅ 过滤name.startswith("."),并排除自己生成的__tmp__前缀,避免二次运行把临时文件当成目标


总结




阶段做什么关键点



规划算新名字用stem/suffix,别手工切点

校验查冲突与存在一处冲突就中止,不部分执行

执行两阶段改名临时名过渡,跨平台安全

兜底dry-run默认不落盘,确认再动手



批量改名的核心不是正则写得多花,而是"先算清楚再动手、冲突就整体中止、用两阶段避免交叉撞车"。把这三条落实,--apply这个开关按下去的时候才不会心慌。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 3:25:36

微信本地聊天记录结构化导出:从.db解密到Obsidian双链笔记

1. 项目本质与真实价值:不是“微信开源”,而是打通本地知识流的桥梁很多人看到标题里的“开源微信流”第一反应是:微信官方把聊天记录协议开源了?或者有某个项目逆向出了微信PC端的完整通信协议?这完全是个误解。微信作…

作者头像 李华
网站建设 2026/10/17 3:24:33

DTC品牌从0到30亿:选品、广告与复购的完整增长打法

1. 先拆掉“30亿”这个数字:DTC的盘子到底是怎么堆出来的可能有人看到“从0到30亿”的第一反应是标题党。我在跨境圈摸爬滚打这些年,见过拆解各种大卖案例的文章,最让人上头的往往是“他家广告素材多牛”“爆款视频播放量几千万”&#xff0c…

作者头像 李华
网站建设 2026/10/17 3:24:13

医疗Agent提示词调优:监控数据驱动的闭环实战

(博主亲历笔记,前情见正文。)7. 从监控数据到提示调整的闭环策略前面的链路标识、语义漂移、上下文膨胀、幻觉归因,都是“发现问题”的手段。但从运维实战角度看,发现问题只是开始,真正决定系统上限的是&am…

作者头像 李华
网站建设 2026/10/17 3:23:09

AI驱动文档搜索系统实战:从全文检索到段落级问答的迭代之路

“AI驱动文档搜索系统”不是一个概念产品,而是一个真正落地到日常使用场景的工程系统。它要解决的是“我明明记得团队里有某份文档,但就是搜不到”以及“搜到一堆文件,却不知道哪一段才是答案”这两件让人抓狂的事。这篇文章是这个系列的第二…

作者头像 李华
网站建设 2026/10/17 3:22:38

Eclipse配置C++开发环境:从JDK到MinGW-w64的完整指南

前阵子接手一个老项目,工程文件是 Eclipse 的工作区结构,代码用 C 写,团队里其他人都在 Windows 上开发。我平时主力用 VS Code,但面对这种组织方式,硬用编辑器去点开一堆 .cproject 和 .settings 反而自找麻烦。于是老…

作者头像 李华
网站建设 2026/10/17 3:22:20

从rrrrr占位命名谈代码规范与项目整理

最近整理工作电脑的时候翻出一个早期草稿,文件名就是"rrrrr",打开一看是某次项目偏差复盘记录。当时随手敲了一串r当占位符,后来就一直没改。这事让我想了挺多——在实际开发里,这种看起来"没意义"的命名其实…

作者头像 李华