Python批量重命名文件:自然排序与防覆盖脚本实现
批量重命名文件看似只是改名,真正落地时要拆成规则层、映射层和安全层。规则层决定新名字怎样生成;映射层解决旧文件和新文件如何一一对应;安全层处理覆盖、备份和回滚。原文强调:文件系统返回的列表顺序不一定和资源管理器显示顺序一致,直接按顺序编号很容易出现 page10 排在 page2 前面。核心 API:os.rename 与 Path.rename
os.rename(src, dst) 传入两个路径字符串,兼容性老;pathlib 的 Path.rename(target) 更顺手,Path 对象可用 .name、.suffix、.stem、.with_name()、.with_suffix()。新代码优先 pathlib,因为 Windows 路径反斜杠在普通字符串里是转义符,而 Path('D:/new/file.txt') 对正斜杠兼容更好。Path.replace() 与 Path.rename() 不同:在 Windows 上目标文件已存在时,os.rename() 会报错,Path.replace() 会静默覆盖,必须警惕。
from pathlib import Path
p = Path('D:/项目照片/IMG_20250413_093021.jpg')
print(p.parent)
print(p.stem)
print(p.suffix)
排序:批量重命名最容易翻车的一环
字符串排序按字符比较,'page10.txt' 会排在 'page2.txt' 前面。可做自然排序,把数字段转为整数;也可按修改时间等元数据排序。永远不要假设文件系统返回顺序就是你要的顺序,要用 sort() 显式控制。
import re
def natural_key(name):
return
from pathlib import Path
files = list(Path('D:/照片').glob('*'))
files.sort(key=lambda p: p.stat().st_mtime)
防覆盖、格式统一、备份
执行前用 Counter 检查新名称是否批量重复;序号用 f'{idx:03d}' 零填充,避免 100 排在 2 前面;重要操作前把 old_name、new_name 写入 CSV 日志,方便回滚。dry_run=True 先试运行打印操作,确认后再改 False。
from collections import Counter
new_names =
duplicates =
if duplicates:
raise SystemExit(f'发现重名: {duplicates}')
场景:按序号批量重命名
下面函数过滤子目录,只处理文件,按自然排序编号,并在目标已存在时跳过。
from pathlib import Path
import re
def natural_key(name):
return
def rename_by_sequence(directory, new_prefix='文件', digits=3):
directory = Path(directory)
files =
files.sort(key=lambda p: natural_key(p.stem))
for idx, file_path in enumerate(files, start=1):
new_name = f'{new_prefix}_{idx:0{digits}d}{file_path.suffix}'
new_path = file_path.with_name(new_name)
if new_path.exists():
print(f'[跳过] 目标已存在: {new_path.name}')
continue
file_path.rename(new_path)
print(f'[成功] {file_path.name} -> {new_path.name}')
调用:rename_by_sequence('D:/照片', new_prefix='现场照', digits=3)。
场景:替换、清除关键字
str.replace() 会替换所有匹配子串,不是只换第一个;只替换第一次可传 replace(old, new, 1)。它区分大小写,若要忽略大小写可用 re.sub(r'(?i)img', '照片', file_path.stem)。
from pathlib import Path
def rename_replace(directory, old_text='', new_text='', remove_text=''):
directory = Path(directory)
for file_path in directory.iterdir():
if not file_path.is_file():
continue
new_stem = file_path.stem
if old_text:
new_stem = new_stem.replace(old_text, new_text)
if remove_text:
new_stem = new_stem.replace(remove_text, '')
new_name = f'{new_stem}{file_path.suffix}'
new_path = file_path.with_name(new_name)
if new_name == file_path.name:
continue
if new_path.exists():
print(f'[跳过] 目标已存在: {new_path.name}')
continue
file_path.rename(new_path)
print(f'[成功] {file_path.name} -> {new_path.name}')
场景:用正则提取关键信息
正则适合从旧文件名提取日期、楼栋、楼层等字段再重组。尽量用 re.match() 严格从开头匹配;命名分组 (?P<date>...) 比数字分组可读性好;文件名中的点号要写成 \.,不要写成任意字符 .。
from pathlib import Path
import re
def rename_extract(directory, pattern, new_format):
directory = Path(directory)
for file_path in directory.iterdir():
if not file_path.is_file():
continue
match = re.match(pattern, file_path.stem)
if not match:
print(f'[跳过] 不匹配规则: {file_path.name}')
continue
try:
new_stem = new_format.format(*match.groups())
except IndexError:
print(f'[错误] 格式中引用了不存在的分组: {file_path.name}')
continue
new_path = file_path.with_name(new_stem + file_path.suffix)
if new_path.exists():
print(f'[跳过] 目标已存在: {new_path.name}')
continue
file_path.rename(new_path)
print(f'[成功] {file_path.name} -> {new_path.name}')
调用示例:
rename_extract(
'D:/巡检记录',
r'(\d{4})-(\d{2})-(\d{2})_(\w+)_(\d+)层',
'{4}_{5}_{1}{2}{3}'
)
场景:时间戳归档与同秒防覆盖
按 st_mtime 生成时间戳时,同一秒内可能有多个文件。循环检查 new_path.exists(),重名时追加 01、02 计数,避免覆盖。
from pathlib import Path
from datetime import datetime
def rename_by_time(directory, prefix='备份'):
directory = Path(directory)
for file_path in directory.iterdir():
if not file_path.is_file():
continue
mtime = file_path.stat().st_mtime
time_str = datetime.fromtimestamp(mtime).strftime('%Y%m%d_%H%M%S')
new_name = f'{prefix}_{time_str}{file_path.suffix}'
new_path = file_path.with_name(new_name)
if new_path.exists():
counter = 1
while new_path.exists():
new_name = f'{prefix}_{time_str}_{counter:02d}{file_path.suffix}'
new_path = file_path.with_name(new_name)
counter += 1
file_path.rename(new_path)
print(f'[成功] {file_path.name} -> {new_path.name}')
兼容与排查
Windows 文件名不允许包含 \ / : * ? " < > |,Linux 只不允许 / 和空字符。如果新文件名用 2025-06-01 12:30 这种带冒号格式,在 Windows 上会触发 FileNotFoundError 或 OSError,建议改成 20250601_1230。中文文件名乱码通常和脚本文件编码、终端编码有关;排序错乱则优先检查是否用了自然排序和零填充。批量操作后,可对比 rename_log.csv,或先 dry_run 打印映射关系再执行。
小结
批量重命名真正可用的结构是:对外暴露一行调用,对内完成排序、防重、备份、日志和 dry_run。Path.rename()/os.rename() 只是动作,规则层、映射层和安全层才是脚本稳定的关键。
Re: Python批量重命名文件:自然排序与防覆盖脚本实现
楼主这篇整理得很实在,批量重命名确实不是改个名那么简单。规则层、映射层、安全层这个拆法很清楚,尤其是自然排序那块,page10 和 page2 的问题很常见,直接用文件系统返回顺序编号基本会翻车。用 pathlib 和 natural_key 显式排序确实更稳。防覆盖部分也很关键,Counter 查重、零填充、dry_run、CSV 日志这些步骤都能省很多麻烦,Path.replace 在 Windows 下会静默覆盖的提醒也值得记住。看最后 rename_replace 的代码似乎没贴完,正式用的时候记得把目标已存在时的处理补上,否则替换关键字场景也容易碰到意外覆盖。整体思路很有参考价值,先试运行再落地这点尤其重要。Re: Python批量重命名文件:自然排序与防覆盖脚本实现
这个拆成规则层、映射层和安全层的思路很清晰,尤其是自然排序那一段,确实很容易被忽略。很多人直接按 glob 或 iterdir 返回的顺序编号,结果 page10 跑到 page2 前面,后面再改就麻烦了。用 re.split(r'(\d+)') 做 natural_key 很实用,按修改时间排序也是个好补充。 防覆盖部分考虑得挺全:先用 Counter 查重,再零填充,dry_run 先打印,重要操作前写 CSV 日志方便回滚。Path.replace 在 Windows 上目标存在时会静默覆盖这一点也值得反复提醒,批量重命名最怕这种不知不觉的覆盖。 按序号重命名那个函数里,目标已存在就跳过比较稳妥,不过如果中间跳过几个,编号可能会不连续。如果希望编号连续,可能得先收集可用目标名再统一映射。替换关键字那段用 str.replace 和 re.sub 忽略大小写也很清楚。 整体很接地气,回头拿测试目录先跑 dry_run 试试,确认没问题再上正式文件。Re: Python批量重命名文件:自然排序与防覆盖脚本实现
楼主这篇总结得挺到位,尤其是把批量重命名拆成规则层、映射层、安全层,实际写脚本时按这个思路走会稳很多。自然排序那个坑太真实了,我之前也遇到过 page10 排在 page2 前面,后来也是用 re.split 把数字段转整数才正常。pathlib 的 with_name、with_suffix 确实比手拼字符串省心,Windows 路径用正斜杠也少很多转义麻烦。 Path.replace 和 os.rename 在 Windows 上的差异提醒得很好,静默覆盖真的危险,我一般宁可先判断 exists 再决定跳过还是报错。dry_run 加 CSV 日志这套也很实用,先打印确认,再真正执行,出问题还能回滚。Counter 查重和三位零填充也是批量编号必备,不然 100 排在 2 前面、重名覆盖都够头疼。感谢分享,先收藏了。
页:
[1]