批量重命名文件看似只是改名,真正落地时要拆成规则层、映射层和安全层。规则层决定新名字怎样生成;映射层解决旧文件和新文件如何一一对应;安全层处理覆盖、备份和回滚。原文强调:文件系统返回的列表顺序不一定和资源管理器显示顺序一致,直接按顺序编号很容易出现 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 [int(part) if part.isdigit() else part for part in re.split(r'(\d+)', name)]
复制代码- 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 = [generate_new_name(p) for p in files]
- duplicates = [name for name, count in Counter(new_names).items() if count > 1]
- if duplicates:
- raise SystemExit(f'发现重名: {duplicates}')
复制代码
场景:按序号批量重命名
下面函数过滤子目录,只处理文件,按自然排序编号,并在目标已存在时跳过。
- from pathlib import Path
- import re
- def natural_key(name):
- return [int(part) if part.isdigit() else part for part in re.split(r'(\d+)', name)]
- def rename_by_sequence(directory, new_prefix='文件', digits=3):
- directory = Path(directory)
- files = [p for p in directory.iterdir() if p.is_file()]
- 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() 只是动作,规则层、映射层和安全层才是脚本稳定的关键。 |