第2章 2.5 综合实战:文件批量重命名工具

🎯 开场:为什么你需要一个批量重命名工具?

上一章我们学会了用 events 事件触发器来监听文件变化——听起来挺酷对吧?但光看不练,很快就会忘。

今天我们来做一个真的能用的小工具:文件批量重命名工具

你有没有遇到过这些情况?

  • 下载了100张照片,名字全是 DSC_0001.jpgDSC_0002.jpg... 想改成 旅行照_2024_01.jpg 这种有意义的名字
  • 整理文件夹时,发现一堆文件名前面多了 副本 - 或者 _final_v3 这种垃圾字符
  • 想给所有文件前面加个日期,或者统一改成「001、002、003」编号

一个个改?手都要废掉。写个脚本?太复杂了。

今天学完,你就能用 50 行代码 写一个你自己的批量重命名工具,而且还能加事件监听——每重命名一个文件,就给你播报一声「好了!」。


🧱 基础:核心概念扫盲(5分钟)

什么是文件批量重命名?

生活类比:想象你有一抽屉的文件夹,上面写的都是「文件夹1」「文件夹2」……你想给它们改成「合同」「发票」「保单」这样有意义的名称。一个一个改太慢,但你有一张「改名前后对照表」,按表批量改,瞬间搞定。

技术本质:就是 遍历文件列表 → 按规则生成新名字 → 重命名。核心就是三步走。

pathlib:Python 里操作文件路径的神器

Node.js 里有 path 模块,Python 里有 pathlib(更现代化)。

from pathlib import Path

# 创建一个路径对象
p = Path("./photos")

# 遍历所有文件
for file in p.glob("*.jpg"):
print(file.name)        # 打印文件名(含后缀)
print(file.stem)        # 打印文件名(不含后缀)
print(file.suffix)      # 打印后缀名(.jpg)

配图1 - 配图1

什么是文件事件监听?

生活类比:你订了一份外卖,你不需要一直盯着手机看外卖到哪了——外卖到了骑手会主动打电话通知你。事件监听就是这个道理:不用一直问「好了没」,好了它自然会告诉你

上一章 events 就是干这个的。我们今天给重命名工具加上事件播报,每重命名完一个文件就触发一个「完成」事件。


🔥 实战:3 个递进项目

项目 1:最最简单的批量重命名(5分钟)

目标:把 photos 文件夹里所有 .jpg 文件改成 照片_001.jpg 这种格式。

from pathlib import Path

def batch_rename():
folder = Path("./photos")
files = sorted(folder.glob("*.jpg"))  # 获取所有 jpg 文件

for index, file in enumerate(files, start=1):
    # 生成新名字:照片_001.jpg
    new_name = f"照片_{index:03d}.jpg"  # :03d = 保留3位,不够前面补0
    new_path = folder / new_name

    # 重命名
    file.rename(new_path)
    print(f"✅ {file.name} → {new_name}")

batch_rename()

预期输出

✅ DSC_0001.jpg → 照片_001.jpg
✅ DSC_0002.jpg → 照片_002.jpg
✅ DSC_0003.jpg → 照片_003.jpg

说白了:就是遍历文件列表,按「前缀_序号.后缀」的规则重新起名。


项目 2:读取配置文件,按规则批量重命名(15分钟)

真实场景:你想根据一个 CSV 文件里的映射关系来重命名。比如 CSV 长这样:

原文件名,新文件名
DSC_0001.jpg,旅行照_北京.jpg
DSC_0002.jpg,旅行照_上海.jpg
import csv
from pathlib import Path

def batch_rename_from_csv(csv_path):
folder = Path("./photos")

# 读取 CSV 配置
with open(csv_path, "r", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    mappings = list(reader)

success_count = 0

for mapping in mappings:
    old_name = mapping["原文件名"].strip()
    new_name = mapping["新文件名"].strip()

    old_path = folder / old_name
    new_path = folder / new_name

    if old_path.exists():
        old_path.rename(new_path)
        print(f"✅ {old_name} → {new_name}")
        success_count += 1
    else:
        print(f"⚠️  文件不存在:{old_name}")

print(f"\n完成!共重命名 {success_count} 个文件")

# 使用
batch_rename_from_csv("./rename_config.csv")

预期输出

✅ DSC_0001.jpg → 旅行照_北京.jpg
✅ DSC_0002.jpg → 旅行照_上海.jpg
⚠️  文件不存在:DSC_0099.jpg

完成!共重命名 2 个文件

说白了:CSV 就是一张「改名前后对照表」,程序读进来,按表一个个改。


项目 3:带事件通知的批量重命名工具(15分钟)

目标:做一个真正的 CLI 工具,带进度提示和事件通知。

import csv
from pathlib import Path
from dataclasses import dataclass
from typing import Callable, List

@dataclass
class RenameEvent:
old_name: str
new_name: str
status: str  # "success" or "error"

class BatchRenamer:
def __init__(self, folder_path: str):
    self.folder = Path(folder_path)
    self.listeners: List[Callable] = []

# 注册监听器(类似 Node.js 的 on())
def on(self, event_name: str, callback: Callable):
    if event_name == "rename":
        self.listeners.append(callback)

# 触发事件(类似 Node.js 的 emit())
def emit(self, event: RenameEvent):
    for listener in self.listeners:
        listener(event)

def rename_file(self, old_name: str, new_name: str):
    old_path = self.folder / old_name
    new_path = self.folder / new_name

    try:
        if not old_path.exists():
            raise FileNotFoundError(f"文件不存在:{old_name}")

        old_path.rename(new_path)
        self.emit(RenameEvent(old_name, new_name, "success"))
    except Exception as e:
        self.emit(RenameEvent(old_name, new_name, f"error: {e}"))

def run(self, csv_path: str):
    print(f"📂 开始批量重命名,当前目录:{self.folder}")
    print("-" * 40)

    with open(csv_path, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        mappings = list(reader)

    total = len(mappings)

    for index, mapping in enumerate(mappings, 1):
        old_name = mapping["原文件名"].strip()
        new_name = mapping["新文件名"].strip()
        self.rename_file(old_name, new_name)
        print(f"进度:{index}/{total}")

    print("-" * 40)
    print("🎉 全部完成!")


# ===== 使用示例 =====
def my_listener(event: RenameEvent):
if event.status == "success":
    print(f"🔔 事件触发:{event.old_name} 已改名为 {event.new_name}")
else:
    print(f"❌ 事件触发:{event.old_name} 出错了 - {event.status}")


# 创建重命名器
renamer = BatchRenamer("./photos")
renamer.on("rename", my_listener)  # 注册监听器
renamer.run("./rename_config.csv")

预期输出

📂 开始批量重命名,当前目录:/Users/xiaoming/photos
----------------------------------------
🔔 事件触发:DSC_0001.jpg 已改名为 旅行照_北京.jpg
进度:1/3
🔔 事件触发:DSC_0002.jpg 已改名为 旅行照_上海.jpg
进度:2/3
❌ 事件触发:DSC_0099.jpg 出错了 - 文件不存在:DSC_0099.jpg
进度:3/3
----------------------------------------
🎉 全部完成!

配图2 - 配图2

说白了:就是用观察者模式给重命名过程加了「广播通知」,每重命名一个就通知所有订阅者。


💪 进阶:常见坑 + 性能小贴士

❌ 坑 1:中文编码问题

Windows 系统默认编码不是 UTF-8,直接读写中文文件名可能报错。

# ❌ 错误写法
with open("rename_config.csv", "r") as f:
...

# ✅ 正确写法
with open("rename_config.csv", "r", encoding="utf-8") as f:
...

❌ 坑 2:重命名目标文件已存在

覆盖了重要文件,哭都来不及。

# ❌ 危险写法
new_path.rename(old_path)  # 直接覆盖

# ✅ 安全写法
if new_path.exists():
print(f"⚠️ 目标文件已存在,跳过:{new_path}")
else:
old_path.rename(new_path)

❌ 坑 3:相对路径踩的坑

# ❌ 可能出问题的写法
folder = Path("./photos")  # 相对路径可能对不上

# ✅ 稳妥写法
folder = Path(__file__).parent / "photos"  # 相对于脚本所在目录

❌ 坑 4:文件被占用时 rename 会失败

# ✅ 加个重试机制
import time

def safe_rename(old_path, new_path, retries=3):
for i in range(retries):
    try:
        old_path.rename(new_path)
        return True
    except PermissionError:
        if i < retries - 1:
            time.sleep(0.5)
        else:
            raise
return False

💡 调试技巧:print 大法

# 在关键步骤加上 print,打印中间状态
def rename_file(self, old_name, new_name):
print(f"[DEBUG] 准备重命名:{old_name} → {new_name}")  # 加这行
old_path = self.folder / old_name
new_path = self.folder / new_name
print(f"[DEBUG] 实际路径:{old_path} → {new_path}")    # 加这行
# ...

✏️ 练习题

练习 1(2分钟):改个前缀

  • 输入:文件夹里有 a.jpg, b.jpg, c.jpg
  • 预期输出:改成 照片_a.jpg, 照片_b.jpg, 照片_c.jpg
  • 提示:只需要改项目 1 里生成新名字的那一行

练习 2(3分钟):加个条件判断

  • 输入:在练习 1 基础上,只改以 a 开头的文件
  • 预期输出:只有 a.jpg 改成 照片_a.jpg,其他不动
  • 提示:加一个 if 判断 file.stem.startswith("a")

练习 3(5分钟):处理新数据格式

  • 输入:有一个 JSON 文件 config.json,内容是 {"mappings": [{"from": "旧.txt", "to": "新.txt"}]}
  • 预期输出:按 JSON 里的映射关系重命名
  • 提示:用 json.load() 读取,其他逻辑和项目 2 一样

练习 4(5分钟):串起两个项目

  • 输入:用项目 2 的 CSV 读取 + 项目 3 的事件机制
  • 预期输出:每重命名一个文件,console 输出 [通知] xxx 已处理
  • 提示:注册一个 listener,callback 里打印 [通知] {event.new_name} 已处理

练习 5(5分钟):报错分析

  • 输入:用户运行脚本后报错 FileNotFoundError: [Errno 2] No such file or directory: 'photo.jpg'
  • 预期输出:说出 3 个可能的原因
  • 提示:考虑文件名拼写、大小写、路径问题

作业:做一个带 GUI 提示的批量重命名工具

需求描述:做一个更实用的命令行工具,可以处理多种重命名规则。

功能点
1. 支持 3 种模式:序号模式(001、002)、日期模式(自动从文件属性读)、前缀模式(用户指定前缀)
2. 带dry-run模式(只显示预览,不实际改名)
3. 支持 -v 参数显示详细信息

加分项
1. 用 argparse 做命令行参数解析
2. 加个统计功能:重命名成功率

验收标准
- 能跑起来
- python rename.py --mode sequence photos/ 能把 photos 里的文件改成序号格式
- python rename.py --mode sequence photos/ --dry-run 只预览不实际改名


📚 总结

今天我们学了 3 个核心点:
1. pathlib 是 Python 里操作文件路径的标准方式,比 os.path 更直观
2. CSV/JSON 配置文件 让重命名规则和代码分离,灵活又方便
3. 观察者模式(事件监听)让程序可以「播报」自己的状态

下一章预告:你可能注意到了,项目 3 的代码里用了一堆嵌套的回调函数——每处理一个文件就要调用一次 listener,代码越写越「歪」。下一章我们要聊聊这个「回调地狱」问题,看看怎么用 Promise 来解决这个问题。敬请期待!


推荐资源
- Python pathlib 官方文档
- 《Python编程:从入门到实践》 第 9 章
- 视频:B站「Python 文件操作合集」

互动钩子:你在工作中有没有遇到过需要批量处理文件的场景?是用什么工具解决的?评论区聊聊,说不定你的方法比我的还巧妙!老粉优先回复~

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。