大话批量重命名工具:Python实现详解
各位看官今天咱们要聊的这个项目可不是什么高深莫测的人工智能也不是什么复杂难懂的分布式系统。它就是一个普普通通的批量重命名工具。但别小看它麻雀虽小五脏俱全。咱们今天就来好好唠唠这个工具是怎么用Python一点点搭建起来的。第一章需求分析——从手动改文件名说起话说在很久很久以前有一个程序员他有一堆电子书文件名乱七八糟的【精品】三体全集_精校版.epub 【转载】百年孤独_[哥伦比亚]加西亚·马尔克斯.pdf 【高清】红楼梦_曹雪芹.epub他想把这些文件名整理得干净一点变成三体全集.epub 百年孤独.pdf 红楼梦.epub于是他开始手动一个个改改到第三个文件的时候他崩溃了——文件太多了需求就这样产生了批量处理文件重命名支持移除前缀、后缀、中间任意字符串支持替换字符串支持修改扩展名支持添加前缀、后缀这就好比你有一堆乱码的书籍你需要一个图书管理员来帮你整理。第二章项目架构——麻雀虽小五脏俱全咱们先来看看这个项目的目录结构ChangeFileName/ ├── src/ │ ├── __init__.py │ ├── main.py # 主程序入口核心逻辑 │ ├── cli.py # 命令行参数解析 │ ├── gui.py # Tkinter GUI界面 │ ├── qt_gui/ # Qt5 GUI界面 │ │ ├── __init__.py │ │ └── main_window.py │ ├── file_scanner.py # 文件扫描模块 │ ├── renamer.py # 重命名核心算法 │ └── history.py # 历史记录管理 ├── tests/ # 单元测试 ├── dist/ # 打包产物 └── README.md # 使用说明这个架构就像一个小工厂file_scanner.py是采购员负责去各个目录采购文件renamer.py是加工车间负责对文件名进行各种加工history.py是仓库负责保存历史记录方便退货main.py是厂长负责协调各个部门gui.py和qt_gui/是销售部负责和用户打交道第三章文件扫描——目录树的深度遍历咱们先来聊聊文件扫描这个模块。这个模块的核心任务就是遍历指定目录找出符合条件的文件。3.1 单线程扫描——一个人的长征最开始咱用的是单线程扫描def scan_directory(directory: str, extensions: List[str], depth: int 1) - List[Tuple[Path, str]]: result [] dir_path Path(directory) if depth 1: # 只扫当前目录就像在自家院子里找东西 for file_path in dir_path.iterdir(): if file_path.is_file(): # 检查扩展名是否匹配 file_ext file_path.suffix.lower().strip(.) if file_ext in ext_set: result.append((file_path, file_path.name)) elif depth 0: # 无限深度扫描就像孙悟空钻进铁扇公主的肚子里 for file_path in dir_path.rglob(*): if file_path.is_file(): file_ext file_path.suffix.lower().strip(.) if file_ext in ext_set: result.append((file_path, file_path.name)) else: # 指定深度扫描就像挖井挖到指定深度就停 for file_path in dir_path.rglob(*): if file_path.is_file(): rel_path file_path.relative_to(dir_path) if len(rel_path.parts) depth: file_ext file_path.suffix.lower().strip(.) if file_ext in ext_set: result.append((file_path, file_path.name)) return result这段代码的逻辑很简单depth1只扫描当前目录用iterdir()depth0无限深度扫描用rglob(*)depthn扫描指定深度用rglob(*)然后判断相对路径的层级3.2 多线程扫描——众人拾柴火焰高后来发现单线程扫描太慢了尤其是文件多的时候。于是咱引入了多线程def scan_directory(directory: str, extensions: List[str], depth: int 1, threads: int 1) - List[Tuple[Path, str]]: result [] dir_path Path(directory) ext_set {ext.lower().strip(.) for ext in extensions} if threads 1: # 单线程老规矩 return _scan_single_directory(dir_path, ext_set, depth, dir_path) # 多线程扫描就像派出多个小分队 from concurrent.futures import ThreadPoolExecutor subdirs [p for p in dir_path.iterdir() if p.is_dir()] with ThreadPoolExecutor(max_workersthreads) as executor: futures [] for subdir in subdirs: futures.append(executor.submit( _scan_single_directory, subdir, ext_set, depth - 1, dir_path )) for future in futures: result.extend(future.result()) # 别忘了扫当前目录 result.extend(_scan_single_directory(dir_path, ext_set, 1, dir_path)) return result多线程扫描的思路是把当前目录下的子目录分配给不同的线程每个线程负责扫描自己的子目录最后合并所有结果这里用到了concurrent.futures.ThreadPoolExecutor这是Python标准库中非常好用的线程池实现。第四章重命名算法——文件名的七十二变重命名算法是这个工具的核心就像孙悟空的七十二变能把文件名变成各种样子。4.1 核心数据结构咱们先看看重命名过程中用到的数据结构class RenameResult: 重命名结果 def __init__(self): self.status ResultStatus.NO_MATCH # 状态 self.changes [] # 变更列表 [(old_name, new_name), ...] self.scanned_files [] # 扫描到的文件列表 self.skipped_files [] # 跳过的文件无效文件名 self.conflict_files [] # 冲突文件新文件名已存在 self.error_message # 错误信息 self.success_count 0 # 成功数量 self.fail_count 0 # 失败数量这个数据结构就像一个账本记录了重命名过程中的所有情况。4.2 文件名处理流程文件名的处理流程就像一条流水线原文件名 → 移除前缀 → 移除后缀 → 移除任意字符串 → 替换字符串 → 修改扩展名 → 添加前缀 → 添加后缀 → 新文件名咱们来看看具体实现def generate_new_filename( filename: str, prefix: Optional[str] None, suffix: Optional[str] None, remove: Optional[List[str]] None, replace: Optional[List[Tuple[str, str]]] None, replace_ext: Optional[Tuple[str, str]] None, add_prefix: Optional[str] None, add_suffix: Optional[str] None, use_regex: bool False, ignore_case: bool False ) - str: 生成新文件名 name, ext os.path.splitext(filename) # 移除前缀 if prefix: name remove_prefix(name, prefix, use_regex, ignore_case) # 移除后缀 if suffix: name remove_suffix(name, suffix, use_regex, ignore_case) # 移除任意字符串 if remove: for s in remove: name remove_string(name, s, use_regex, ignore_case) # 替换字符串 if replace: for old_str, new_str in replace: name replace_string(name, old_str, new_str, use_regex, ignore_case) # 修改扩展名 if replace_ext: old_ext, new_ext replace_ext if old_ext is None or ext.lower() f.{old_ext.lower()}: ext f.{new_ext} # 添加前缀 if add_prefix: name add_prefix name # 添加后缀 if add_suffix: name name add_suffix return name ext这段代码的逻辑很清晰就像按顺序执行一系列操作。4.3 字符串处理函数咱们来看看几个关键的字符串处理函数def remove_prefix(name: str, prefix: str, use_regex: bool False, ignore_case: bool False) - str: 移除前缀 if use_regex: flags re.IGNORECASE if ignore_case else 0 return re.sub(r^ re.escape(prefix), , name, flagsflags) else: if ignore_case: if name.lower().startswith(prefix.lower()): return name[len(prefix):] else: if name.startswith(prefix): return name[len(prefix):] return name这个函数支持两种模式普通模式直接判断是否以指定前缀开头正则模式使用正则表达式匹配def replace_string(name: str, old_str: str, new_str: str, use_regex: bool False, ignore_case: bool False) - str: 替换字符串 if use_regex: flags re.IGNORECASE if ignore_case else 0 return re.sub(old_str, new_str, name, flagsflags) else: if ignore_case: import re return re.sub(re.escape(old_str), new_str, name, flagsre.IGNORECASE) else: return name.replace(old_str, new_str)替换函数也类似支持普通替换和正则替换。第五章历史记录——时光倒流的秘密重命名操作是不可逆的万一改错了怎么办别担心咱有历史记录功能。5.1 历史记录的数据结构class HistoryEntry: 历史记录条目 def __init__(self): self.timestamp 0 # 时间戳 self.changes [] # 变更列表 def to_dict(self): return { timestamp: self.timestamp, changes: self.changes } classmethod def from_dict(cls, data): entry cls() entry.timestamp data[timestamp] entry.changes data[changes] return entry历史记录就像一本日记记录了每次重命名操作。5.2 历史记录的存储历史记录存储在一个JSON文件里放在目标目录的.rename_history文件夹中def save_history(directory: str, changes: List[Tuple[str, str]]): 保存历史记录 history_dir os.path.join(directory, .rename_history) os.makedirs(history_dir, exist_okTrue) entry HistoryEntry() entry.timestamp int(time.time()) entry.changes changes history_file os.path.join(history_dir, f{entry.timestamp}.json) with open(history_file, w, encodingutf-8) as f: json.dump(entry.to_dict(), f, ensure_asciiFalse)每次执行重命名操作后都会保存一条历史记录。5.3 撤销操作——时光倒流撤销操作就是读取最近的一条历史记录然后反向执行重命名def undo_rename(directory: str) - RenameResult: 撤销最近一次重命名操作 result RenameResult() if not has_history(directory): result.error_message 没有可撤销的历史记录 return result entry undo_last(directory) if entry is None: result.error_message 没有可撤销的历史记录 return result # 反向执行重命名 for old_name, new_name in entry.changes: file_path os.path.join(directory, old_name) if os.path.exists(file_path): try: rename_file(Path(file_path), new_name) result.success_count 1 except Exception as e: result.fail_count 1 else: result.fail_count 1 return result这里的关键是历史记录中保存的是(原文件名, 新文件名)撤销时就把新文件名改回原文件名。第六章GUI界面——用户与程序的桥梁一个好的工具不仅要有强大的功能还要有友好的界面。6.1 Tkinter版本——朴实无华Tkinter是Python自带的GUI库就像一个朴素的农家小院虽然不华丽但很实用。class RenameApp: def __init__(self, root): self.root root self.root.title(批量重命名工具) # 创建各种控件 self.directory tk.StringVar() self.extensions tk.StringVar(valueepub pdf) # ... self._build_ui() def _build_ui(self): # 构建界面布局 main_frame ttk.Frame(self.root, padding10) main_frame.pack(filltk.BOTH, expandTrue) notebook ttk.Notebook(main_frame) # 添加配置、文件列表、预览三个选项卡Tkinter的界面很简单用Frame、Label、Entry、Button等控件就能搭建起来。6.2 Qt5版本——华丽转身后来咱用PyQt5重写了GUI就像把农家小院改成了别墅class RenameQtApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(批量重命名工具) self.setGeometry(100, 100, 1000, 750) self.init_ui() def init_ui(self): central_widget QWidget() self.setCentralWidget(central_widget) main_layout QVBoxLayout(central_widget) self.tab_widget QTabWidget() main_layout.addWidget(self.tab_widget) self.build_config_panel() self.build_file_list_panel() self.build_preview_panel()Qt5的界面更加现代化支持更多的样式和功能。6.3 异步文件扫描——界面不卡顿的秘密在GUI版本中文件扫描是一个耗时操作如果在主线程中执行会导致界面卡顿。所以咱用了异步处理Tkinter版本def _refresh_file_list(self): # 在后台线程中执行扫描 def scan_worker(): files scan_directory(directory, self._get_extensions(), self._get_depth(), self._get_threads()) self.root.after(0, self._update_file_list, files) thread threading.Thread(targetscan_worker) thread.daemon True thread.start()Qt5版本class ScanWorker(QThread): finished pyqtSignal(list) def run(self): files scan_directory(self.directory, self.extensions, self.depth, self.threads) self.finished.emit(files) def refresh_file_list(self): self.scan_worker ScanWorker(directory, extensions, depth, threads) self.scan_worker.finished.connect(self.update_file_list) self.scan_worker.start()两种版本都用了后台线程来执行扫描操作避免阻塞主线程。第七章命令行接口——程序员的最爱除了GUI咱还提供了命令行接口方便程序员使用def parse_args(args: Optional[List[str]] None) - argparse.Namespace: parser argparse.ArgumentParser( progbatch-rename, description批量重命名工具 ) parser.add_argument(directory, help目标目录路径) parser.add_argument(-e, --extensions, nargs, default[epub, pdf]) parser.add_argument(-p, --prefix, help要移除的前缀) parser.add_argument(-s, --suffix, help要移除的后缀) parser.add_argument(-r, --remove, actionappend, help要移除的字符串) parser.add_argument(--replace, actionappend, help替换字符串格式 OLD:NEW) parser.add_argument(--replace-ext, metavarOLD:NEW, help替换扩展名) parser.add_argument(--add-prefix, help添加前缀) parser.add_argument(--add-suffix, help添加后缀) parser.add_argument(--regex, actionstore_true, help使用正则表达式) parser.add_argument(--ignore-case, actionstore_true, help忽略大小写) parser.add_argument(--depth, typeint, default1, help遍历深度) parser.add_argument(--threads, typeint, default1, help线程数) parser.add_argument(-d, --dry-run, actionstore_true, help预览模式) parser.add_argument(-f, --force, actionstore_true, help强制执行) parser.add_argument(-u, --undo, actionstore_true, help撤销) parser.add_argument(--gui, choices[tk, qt], help启动GUI) return parser.parse_args(args)使用示例# 移除前缀【精品】 python -m src.main /path/to/dir -p 【精品】 # 替换字符串 python -m src.main /path/to/dir --replace 【转载】: # 修改扩展名 python -m src.main /path/to/dir --replace-ext epub:mobi # 预览模式 python -m src.main /path/to/dir -p 【精品】 -d # 无限深度扫描 python -m src.main /path/to/dir --depth 0 --threads 4第八章打包发布——从代码到可执行文件最后咱用PyInstaller把项目打包成了exe文件方便用户使用# 打包GUI版本 pyinstaller --onefile --windowed --name BatchRenameTool src/main.py # 打包CLI版本 pyinstaller --onefile --name BatchRenameTool_CLI src/main.py打包过程中遇到了一些坑比如缺少platformdirs依赖、Tcl/Tk DLL缺失等但都一一解决了。第九章总结——一个工具的诞生回顾这个项目的开发过程咱用到了技术点实现方式文件扫描Pathlib 递归遍历 多线程字符串处理正则表达式 字符串方法历史记录JSON文件存储GUI界面Tkinter PyQt5命令行接口argparse打包发布PyInstaller这个工具虽然简单但它体现了软件开发的基本流程需求分析明确用户需求架构设计设计合理的模块划分核心实现实现核心功能界面开发提供友好的用户界面测试验证确保功能正确打包发布方便用户使用就像《大话数据结构》里说的数据结构和算法是编程的基本功。同样良好的代码结构和清晰的逻辑也是开发一个好工具的关键。希望这篇文章能让你对这个项目有更深入的了解也希望能给你带来一些启发。如果你有什么想法或建议欢迎留言讨论后记这个项目还在不断进化中说不定哪天就会加入更多神奇的功能。如果你想了解最新动态欢迎关注项目的GitHub仓库。