# Python实现批量文件重命名工具:正则匹配+GUI交互+撤销功能


背景介绍

在日常文件管理中,我们常常需要对大量文件进行重命名(如整理照片、统一文档格式)。手动逐个修改效率低下,因此开发一个批量文件重命名工具能极大提升工作效率。本文将使用Python的Tkinter库结合正则表达式,实现一个支持预览、撤销、扩展名过滤和递归处理的重命名工具。

思路分析

我们将工具拆分为以下核心模块:
1. 文件加载:选择目标文件夹,按扩展名过滤并支持递归遍历子文件夹。
2. 正则匹配与替换:通过正则表达式捕获文件名的指定部分(如日期、前缀),结合自定义格式生成新名。
3. 预览功能:重命名前展示“原→新”文件名对比,避免误操作。
4. 重命名执行:修改文件系统中的文件名,同时记录历史用于撤销。
5. 撤销功能:通过历史记录一键恢复文件原名。
6. GUI交互:使用Tkinter构建直观的操作界面,降低使用门槛。

代码实现

下面是完整的Python代码(基于Tkinter、os、re模块):

import tkinter as tk
from tkinter import ttk, filedialog
import os
import re

class BatchRenamer(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("批量文件重命名工具")
        self.geometry("800x600")
        self.files = []  # 存储加载的文件信息:path, original_name
        self.preview = []  # 预览的文件重命名方案
        self.history = []  # 重命名历史,用于撤销
        self.init_ui()

    def init_ui(self):
        # 文件夹选择区域
        frame_folder = ttk.LabelFrame(self, text="文件夹选择")
        frame_folder.pack(fill="x", padx=10, pady=5)
        ttk.Button(frame_folder, text="选择文件夹", command=self.select_folder).pack(side="left", padx=5, pady=5)
        self.folder_path = tk.StringVar()
        ttk.Entry(frame_folder, textvariable=self.folder_path, width=50).pack(side="left", padx=5, pady=5)
        ttk.Button(frame_folder, text="加载文件", command=self.load_files).pack(side="left", padx=5, pady=5)

        # 过滤与递归选项
        frame_filter = ttk.LabelFrame(self, text="过滤与递归")
        frame_filter.pack(fill="x", padx=10, pady=5)
        ttk.Label(frame_filter, text="扩展名(逗号分隔,如 jpg,png):").pack(side="left", padx=5, pady=5)
        self.extensions = tk.StringVar(value="")
        ttk.Entry(frame_filter, textvariable=self.extensions, width=15).pack(side="left", padx=5, pady=5)
        self.recursive = tk.BooleanVar(value=False)
        ttk.Checkbutton(frame_filter, text="递归子文件夹", variable=self.recursive).pack(side="left", padx=5, pady=5)

        # 正则与替换规则
        frame_regex = ttk.LabelFrame(self, text="正则匹配与替换")
        frame_regex.pack(fill="x", padx=10, pady=5)
        ttk.Label(frame_regex, text="正则模式:").pack(side="left", padx=5, pady=5)
        # 修正正则:捕获扩展名作为第4组(\4)
        self.regex_pattern = tk.StringVar(value=r"^\w+_(\d{4})(\d{2})(\d{2})\.(\w+)$")  
        ttk.Entry(frame_regex, textvariable=self.regex_pattern, width=30).pack(side="left", padx=5, pady=5)
        ttk.Label(frame_regex, text="替换规则:").pack(side="left", padx=5, pady=5)
        self.replace_rule = tk.StringVar(value=r"Travel_\1-\2-\3.\4")  # 示例:格式化日期+保留扩展名
        ttk.Entry(frame_regex, textvariable=self.replace_rule, width=30).pack(side="left", padx=5, pady=5)

        # 功能按钮
        frame_buttons = ttk.Frame(self)
        frame_buttons.pack(fill="x", padx=10, pady=5)
        ttk.Button(frame_buttons, text="预览", command=self.preview_rename).pack(side="left", padx=5)
        ttk.Button(frame_buttons, text="重命名", command=self.execute_rename).pack(side="left", padx=5)
        self.undo_button = ttk.Button(frame_buttons, text="撤销", command=self.undo_rename, state="disabled")
        self.undo_button.pack(side="left", padx=5)

        # 预览/历史列表
        frame_list = ttk.LabelFrame(self, text="预览/历史")
        frame_list.pack(fill="both", expand=True, padx=10, pady=5)
        self.listbox = tk.Listbox(frame_list, width=80, height=20)
        self.listbox.pack(side="left", fill="both", expand=True)
        scrollbar = ttk.Scrollbar(frame_list, orient="vertical", command=self.listbox.yview)
        scrollbar.pack(side="right", fill="y")
        self.listbox.config(yscrollcommand=scrollbar.set)

    def select_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self.folder_path.set(folder)

    def load_files(self):
        folder = self.folder_path.get()
        if not folder:
            self.listbox.insert(tk.END, "请先选择文件夹!")
            return
        # 处理扩展名过滤
        ext_filter = self.extensions.get().strip()
        exts = [e.strip().lower() for e in ext_filter.split(",")] if ext_filter else []
        recursive = self.recursive.get()
        self.files = []
        self.listbox.delete(0, tk.END)  # 清空列表
        try:
            if recursive:
                # 递归遍历所有子文件夹
                for root, _, files in os.walk(folder):
                    for file in files:
                        file_path = os.path.join(root, file)
                        # 过滤扩展名
                        if not exts or any(file.lower().endswith(ext) for ext in exts):
                            self.files.append({"path": file_path, "original": file})
            else:
                # 仅遍历当前文件夹
                for file in os.listdir(folder):
                    file_path = os.path.join(folder, file)
                    if os.path.isfile(file_path):
                        if not exts or any(file.lower().endswith(ext) for ext in exts):
                            self.files.append({"path": file_path, "original": file})
            self.listbox.insert(tk.END, f"已加载 {len(self.files)} 个文件")
        except Exception as e:
            self.listbox.insert(tk.END, f"加载文件出错:{e}")

    def preview_rename(self):
        if not self.files:
            self.listbox.insert(tk.END, "请先加载文件!")
            return
        pattern = self.regex_pattern.get()
        replace = self.replace_rule.get()
        self.preview = []
        self.listbox.delete(0, tk.END)
        for f in self.files:
            original = f["original"]
            try:
                # 正则替换生成新文件名
                new_name = re.sub(pattern, replace, original)
                self.preview.append({"original": original, "new": new_name, "path": f["path"]})
                self.listbox.insert(tk.END, f"原文件名:{original} → 新文件名:{new_name}")
            except re.error as e:
                self.listbox.insert(tk.END, f"正则错误({original}):{e}")
                self.preview.append({"original": original, "new": original, "path": f["path"]})  # 错误时保留原文件名

    def execute_rename(self):
        if not self.preview:
            self.listbox.insert(tk.END, "请先预览!")
            return
        self.history = []  # 初始化历史记录
        success = 0
        fail = 0
        for item in self.preview:
            old_path = item["path"]
            old_name = item["original"]
            new_name = item["new"]
            if old_name == new_name:
                continue  # 文件名未变化,跳过
            new_path = os.path.join(os.path.dirname(old_path), new_name)
            try:
                os.rename(old_path, new_path)
                self.history.append({
                    "old_path": old_path, 
                    "old_name": old_name, 
                    "new_path": new_path, 
                    "new_name": new_name
                })
                success += 1
            except Exception as e:
                self.listbox.insert(tk.END, f"重命名失败({old_name}):{e}")
                fail += 1
        self.listbox.insert(tk.END, f"重命名完成:成功 {success} 个,失败 {fail} 个")
        # 启用撤销按钮
        self.undo_button.config(state="normal")

    def undo_rename(self):
        if not self.history:
            self.listbox.insert(tk.END, "没有可撤销的操作!")
            return
        success = 0
        fail = 0
        # 倒序恢复,避免路径变化影响后续文件
        for record in reversed(self.history):
            old_path = record["old_path"]
            old_name = record["old_name"]
            new_path = record["new_path"]
            try:
                os.rename(new_path, old_path)
                success += 1
            except Exception as e:
                self.listbox.insert(tk.END, f"撤销失败({new_path}):{e}")
                fail += 1
        self.listbox.insert(tk.END, f"撤销完成:成功 {success} 个,失败 {fail} 个")
        self.history = []  # 清空历史
        self.undo_button.config(state="disabled")  # 禁用撤销按钮

if __name__ == "__main__":
    app = BatchRenamer()
    app.mainloop()

代码说明

  1. GUI布局:使用Tkinter的LabelFrameButtonEntryListbox等组件,将功能模块化(文件夹选择、过滤、正则设置、操作按钮、预览列表)。
  2. 文件加载:通过os.walk(递归)或os.listdir(非递归)遍历文件,结合扩展名过滤(支持多扩展名,如jpg,png)。
  3. 正则替换:使用re.sub捕获正则组(如示例中的\1为年份、\4为扩展名),生成新文件名。预览时展示替换效果,避免正则错误。
  4. 重命名与撤销:执行时记录文件的“原路径→新路径”,撤销时反向调用os.rename恢复原名。

功能测试

以示例中的照片文件夹为例:
– 选择文件夹/Users/Documents/TravelPhotos,输入扩展名jpg,mp4,勾选递归(若有子文件夹)。
– 正则模式:^\w+_(\d{4})(\d{2})(\d{2})\.(\w+)$,替换规则:Travel_\1-\2-\3.\4
– 点击“预览”,列表将显示IMG_20240501.jpg → Travel_2024-05-01.jpg等效果。
– 点击“重命名”完成修改,点击“撤销”可恢复原文件名。

总结与拓展

本文实现的批量重命名工具整合了文件操作正则表达式GUI交互数据结构(列表/字典存储状态),适合整理照片、文档等场景。可拓展方向包括:
– 正则模板库:提供“提取数字”“替换前缀”等常用正则,降低使用门槛。
– 进度条显示:大文件量时展示重命名进度。
– 错误处理优化:如文件被占用时的重试机制。

通过本项目,开发者可强化Python文件操作、正则表达式和GUI开发能力,实现更高效的文件管理工具。


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注