commit f32c1653f0364f76f502d60173efe793154be57e Author: Wuqiyang312 Date: Sun Dec 14 22:21:51 2025 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..680a167 --- /dev/null +++ b/.gitignore @@ -0,0 +1,155 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is an error to commit Pipfile.lock with +# cross-platform compatibility, and it should be ignored: +Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak +venv.bak + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# VS Code +.vscode/ + +# Windows + Thumbs.db +ehthumbs.db +Icon? + [Dd]esktop.ini + +# macOS + .DS_Store + +# Batches +*.bat \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d298c3 --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# 文件重命名工具 + +这是一个功能强大的Python文件重命名工具,支持命令行和图形界面两种使用方式。 + +## 功能特性 + +- 批量文件重命名 +- 简单字符串替换 +- 正则表达式替换 +- 添加前缀/后缀 +- 自动编号 +- 大小写转换 +- 预览模式 +- 操作撤销 +- 日志记录 + +## 安装要求 + +- Python 3.6+ +- 标准库(无需额外安装第三方包) + +## 使用方法 + +### 命令行版本 + +```bash +# 查看帮助 +python file_renamer.py -h + +# 简单替换 +python file_renamer.py /path/to/directory replace "old_text" "new_text" + +# 正则表达式替换 +python file_renamer.py /path/to/directory regex "\d+" "#" + +# 添加前缀 +python file_renamer.py /path/to/directory prefix "PRE_" + +# 添加后缀 +python file_renamer.py /path/to/directory suffix "_SUF" + +# 自动编号 +python file_renamer.py /path/to/directory enumerate --prefix="IMG_" --start=1 --digits=3 + +# 大小写转换 +python file_renamer.py /path/to/directory case lower + +# 预览模式(不实际重命名) +python file_renamer.py /path/to/directory replace "old" "new" --preview + +# 撤销上次操作 +python file_renamer.py /path/to/directory undo +``` + +### 图形界面版本 + +```bash +python gui_renamer.py +``` + +图形界面提供了直观的操作方式,支持以下功能: + +1. 选择目录 +2. 多种重命名模式切换 +3. 预览功能 +4. 实时结果显示 +5. 撤销操作 + +## 使用示例 + +### 示例1:批量替换文件名中的日期格式 + +假设有一批文件名为 `report_2023-01-01.txt`,想改为 `report_20230101.txt`: + +```bash +python file_renamer.py /path/to/files replace "-" "" +``` + +### 示例2:使用正则表达式清理文件名 + +移除文件名中的数字: + +```bash +python file_renamer.py /path/to/files regex "\d+" "" +``` + +### 示例3:为照片文件添加前缀和编号 + +```bash +python file_renamer.py /path/to/photos enumerate --prefix="PHOTO_" --start=1 --digits=4 +``` + +这会将文件重命名为:`PHOTO_0001.jpg`, `PHOTO_0002.png`, ... + +### 示例4:统一文件名大小写 + +```bash +python file_renamer.py /path/to/files case lower +``` + +## 注意事项 + +1. 操作前建议先使用预览模式查看结果 +2. 工具会自动避免文件名冲突 +3. 所有操作都会记录到日志文件中 +4. 支持通过撤销功能回退最后一次操作 +5. 正则表达式功能需要熟悉基本的正则语法 + +## 日志和历史记录 + +- 日志文件:`rename_log.txt` +- 历史记录:`rename_history.json` + +可以通过历史记录文件查看所有重命名操作,并在需要时手动恢复文件名。 \ No newline at end of file diff --git a/example_usage.py b/example_usage.py new file mode 100644 index 0000000..77f6691 --- /dev/null +++ b/example_usage.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +文件重命名工具使用示例 +""" + +from file_renamer import FileRenamer + + +def main(): + """使用示例""" + # 创建重命名工具实例 + renamer = FileRenamer() + + # 设置要处理的目录 + directory = "./test_files" # 请修改为实际的目录路径 + + print("文件重命名工具使用示例") + print("=" * 30) + + # 1. 简单字符串替换 + print("1. 简单字符串替换:") + print("将目录中所有文件名中的 'old' 替换为 'new'") + count = renamer.simple_rename(directory, "old", "new", preview=True) + print(f"预览模式下将重命名 {count} 个文件") + + # 2. 正则表达式替换 + print("\n2. 正则表达式替换:") + print("将文件名中的数字替换为 # 符号") + count = renamer.regex_rename(directory, r"\d+", "#", preview=True) + print(f"预览模式下将重命名 {count} 个文件") + + # 3. 添加前缀 + print("\n3. 添加前缀:") + print("为所有文件添加 'PREFIX_' 前缀") + count = renamer.add_prefix(directory, "PREFIX_", preview=True) + print(f"预览模式下将重命名 {count} 个文件") + + # 4. 添加后缀 + print("\n4. 添加后缀:") + print("为所有文件添加 '_SUFFIX' 后缀") + count = renamer.add_suffix(directory, "_SUFFIX", preview=True) + print(f"预览模式下将重命名 {count} 个文件") + + # 5. 自动编号 + print("\n5. 自动编号:") + print("为文件添加编号") + count = renamer.enumerate_files(directory, prefix="FILE_", start_number=1, digits=3, preview=True) + print(f"预览模式下将重命名 {count} 个文件") + + # 6. 大小写转换 + print("\n6. 大小写转换:") + print("将所有文件名转换为小写") + count = renamer.change_case(directory, "lower", preview=True) + print(f"预览模式下将重命名 {count} 个文件") + + print("\n" + "=" * 30) + print("如需实际执行重命名,请将 preview 参数设为 False") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/file_renamer.py b/file_renamer.py new file mode 100644 index 0000000..ba9c7a5 --- /dev/null +++ b/file_renamer.py @@ -0,0 +1,422 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +文件重命名工具 +支持批量重命名、正则表达式、预览模式等功能 +""" + +import os +import re +import json +import logging +from datetime import datetime +from pathlib import Path + + +class FileRenamer: + def __init__(self, log_file="rename_log.txt"): + """初始化重命名工具""" + self.rename_history = [] + self.setup_logging(log_file) + + def setup_logging(self, log_file): + """设置日志记录""" + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_file, encoding='utf-8'), + logging.StreamHandler() + ] + ) + self.logger = logging.getLogger(__name__) + + def add_to_history(self, old_path, new_path): + """添加重命名历史记录""" + self.rename_history.append({ + 'timestamp': datetime.now().isoformat(), + 'old_path': old_path, + 'new_path': new_path + }) + + def save_history(self, history_file="rename_history.json"): + """保存重命名历史到文件""" + try: + with open(history_file, 'w', encoding='utf-8') as f: + json.dump(self.rename_history, f, ensure_ascii=False, indent=2) + self.logger.info(f"历史记录已保存到 {history_file}") + except Exception as e: + self.logger.error(f"保存历史记录失败: {e}") + + def load_history(self, history_file="rename_history.json"): + """从文件加载重命名历史""" + try: + if os.path.exists(history_file): + with open(history_file, 'r', encoding='utf-8') as f: + self.rename_history = json.load(f) + self.logger.info(f"历史记录已从 {history_file} 加载") + except Exception as e: + self.logger.error(f"加载历史记录失败: {e}") + + def undo_last_rename(self): + """撤销最后一次重命名操作""" + if not self.rename_history: + self.logger.warning("没有可撤销的操作") + return False + + last_operation = self.rename_history.pop() + old_path = last_operation['old_path'] + new_path = last_operation['new_path'] + + # 检查文件是否存在 + if not os.path.exists(new_path): + self.logger.error(f"文件 {new_path} 不存在,无法撤销") + return False + + try: + os.rename(new_path, old_path) + self.logger.info(f"已撤销重命名: {new_path} -> {old_path}") + self.save_history() + return True + except Exception as e: + self.logger.error(f"撤销操作失败: {e}") + # 如果撤销失败,将操作重新加入历史记录 + self.rename_history.append(last_operation) + return False + + def list_files(self, directory, pattern=None): + """列出目录中的文件""" + try: + path = Path(directory) + if not path.exists(): + self.logger.error(f"目录 {directory} 不存在") + return [] + + files = [] + for item in path.iterdir(): + if item.is_file(): + if pattern is None or re.search(pattern, item.name): + files.append(item) + + files.sort(key=lambda x: x.name) + return files + except Exception as e: + self.logger.error(f"列出文件时出错: {e}") + return [] + + def simple_rename(self, directory, search_pattern, replace_pattern, preview=False): + """简单的字符串替换重命名""" + files = self.list_files(directory) + if not files: + self.logger.warning("目录中没有找到文件") + return 0 + + renamed_count = 0 + + for file_path in files: + old_name = file_path.name + new_name = old_name.replace(search_pattern, replace_pattern) + + if old_name != new_name: + new_path = file_path.parent / new_name + + # 检查目标文件是否已存在 + if new_path.exists(): + self.logger.warning(f"文件 {new_name} 已存在,跳过 {old_name}") + continue + + if preview: + print(f"[预览] {old_name} -> {new_name}") + else: + try: + os.rename(file_path, new_path) + self.add_to_history(str(file_path), str(new_path)) + self.logger.info(f"已重命名: {old_name} -> {new_name}") + renamed_count += 1 + except Exception as e: + self.logger.error(f"重命名 {old_name} 失败: {e}") + + if not preview and renamed_count > 0: + self.save_history() + + return renamed_count + + def regex_rename(self, directory, regex_pattern, replace_pattern, preview=False): + """使用正则表达式的重命名""" + files = self.list_files(directory) + if not files: + self.logger.warning("目录中没有找到文件") + return 0 + + renamed_count = 0 + try: + compiled_pattern = re.compile(regex_pattern) + except re.error as e: + self.logger.error(f"无效的正则表达式 '{regex_pattern}': {e}") + return 0 + + for file_path in files: + old_name = file_path.name + # 使用正则表达式进行替换 + new_name = compiled_pattern.sub(replace_pattern, old_name) + + if old_name != new_name: + new_path = file_path.parent / new_name + + # 检查目标文件是否已存在 + if new_path.exists(): + self.logger.warning(f"文件 {new_name} 已存在,跳过 {old_name}") + continue + + if preview: + print(f"[预览] {old_name} -> {new_name}") + else: + try: + os.rename(file_path, new_path) + self.add_to_history(str(file_path), str(new_path)) + self.logger.info(f"已重命名: {old_name} -> {new_name}") + renamed_count += 1 + except Exception as e: + self.logger.error(f"重命名 {old_name} 失败: {e}") + + if not preview and renamed_count > 0: + self.save_history() + + return renamed_count + + def add_prefix(self, directory, prefix, preview=False): + """为文件名添加前缀""" + files = self.list_files(directory) + if not files: + self.logger.warning("目录中没有找到文件") + return 0 + + renamed_count = 0 + + for file_path in files: + old_name = file_path.name + new_name = prefix + old_name + new_path = file_path.parent / new_name + + # 检查目标文件是否已存在 + if new_path.exists(): + self.logger.warning(f"文件 {new_name} 已存在,跳过 {old_name}") + continue + + if preview: + print(f"[预览] {old_name} -> {new_name}") + else: + try: + os.rename(file_path, new_path) + self.add_to_history(str(file_path), str(new_path)) + self.logger.info(f"已重命名: {old_name} -> {new_name}") + renamed_count += 1 + except Exception as e: + self.logger.error(f"重命名 {old_name} 失败: {e}") + + if not preview and renamed_count > 0: + self.save_history() + + return renamed_count + + def add_suffix(self, directory, suffix, preview=False): + """为文件名添加后缀(在扩展名之前)""" + files = self.list_files(directory) + if not files: + self.logger.warning("目录中没有找到文件") + return 0 + + renamed_count = 0 + + for file_path in files: + old_name = file_path.name + name_without_ext = file_path.stem + ext = file_path.suffix + new_name = name_without_ext + suffix + ext + new_path = file_path.parent / new_name + + # 检查目标文件是否已存在 + if new_path.exists(): + self.logger.warning(f"文件 {new_name} 已存在,跳过 {old_name}") + continue + + if preview: + print(f"[预览] {old_name} -> {new_name}") + else: + try: + os.rename(file_path, new_path) + self.add_to_history(str(file_path), str(new_path)) + self.logger.info(f"已重命名: {old_name} -> {new_name}") + renamed_count += 1 + except Exception as e: + self.logger.error(f"重命名 {old_name} 失败: {e}") + + if not preview and renamed_count > 0: + self.save_history() + + return renamed_count + + def enumerate_files(self, directory, prefix="", start_number=1, digits=3, preview=False): + """为文件添加序号""" + files = self.list_files(directory) + if not files: + self.logger.warning("目录中没有找到文件") + return 0 + + renamed_count = 0 + number = start_number + + for file_path in files: + old_name = file_path.name + ext = file_path.suffix + new_name = f"{prefix}{str(number).zfill(digits)}{ext}" + new_path = file_path.parent / new_name + + # 检查目标文件是否已存在 + if new_path.exists(): + self.logger.warning(f"文件 {new_name} 已存在,跳过 {old_name}") + continue + + if preview: + print(f"[预览] {old_name} -> {new_name}") + else: + try: + os.rename(file_path, new_path) + self.add_to_history(str(file_path), str(new_path)) + self.logger.info(f"已重命名: {old_name} -> {new_name}") + renamed_count += 1 + except Exception as e: + self.logger.error(f"重命名 {old_name} 失败: {e}") + + number += 1 + + if not preview and renamed_count > 0: + self.save_history() + + return renamed_count + + def change_case(self, directory, case_type="lower", preview=False): + """更改文件名大小写""" + files = self.list_files(directory) + if not files: + self.logger.warning("目录中没有找到文件") + return 0 + + renamed_count = 0 + + for file_path in files: + old_name = file_path.name + + if case_type == "lower": + new_name = old_name.lower() + elif case_type == "upper": + new_name = old_name.upper() + elif case_type == "title": + new_name = old_name.title() + else: + self.logger.error(f"不支持的大小写类型: {case_type}") + return 0 + + if old_name != new_name: + new_path = file_path.parent / new_name + + # 检查目标文件是否已存在 + if new_path.exists(): + self.logger.warning(f"文件 {new_name} 已存在,跳过 {old_name}") + continue + + if preview: + print(f"[预览] {old_name} -> {new_name}") + else: + try: + os.rename(file_path, new_path) + self.add_to_history(str(file_path), str(new_path)) + self.logger.info(f"已重命名: {old_name} -> {new_name}") + renamed_count += 1 + except Exception as e: + self.logger.error(f"重命名 {old_name} 失败: {e}") + + if not preview and renamed_count > 0: + self.save_history() + + return renamed_count + + +def main(): + """主函数 - 命令行界面""" + import argparse + + parser = argparse.ArgumentParser(description="文件重命名工具") + parser.add_argument("directory", help="要处理的目录路径") + parser.add_argument("-p", "--preview", action="store_true", help="预览模式,不实际重命名文件") + + # 子命令 + subparsers = parser.add_subparsers(dest="command", help="重命名命令") + + # 简单替换命令 + replace_parser = subparsers.add_parser("replace", help="简单字符串替换") + replace_parser.add_argument("search", help="要查找的字符串") + replace_parser.add_argument("replace", help="替换字符串") + + # 正则表达式命令 + regex_parser = subparsers.add_parser("regex", help="正则表达式替换") + regex_parser.add_argument("pattern", help="正则表达式模式") + regex_parser.add_argument("replace", help="替换字符串") + + # 添加前缀命令 + prefix_parser = subparsers.add_parser("prefix", help="添加前缀") + prefix_parser.add_argument("prefix", help="前缀字符串") + + # 添加后缀命令 + suffix_parser = subparsers.add_parser("suffix", help="添加后缀") + suffix_parser.add_argument("suffix", help="后缀字符串") + + # 序号命令 + enum_parser = subparsers.add_parser("enumerate", help="添加序号") + enum_parser.add_argument("--prefix", default="", help="序号前缀") + enum_parser.add_argument("--start", type=int, default=1, help="起始数字") + enum_parser.add_argument("--digits", type=int, default=3, help="数字位数") + + # 大小写命令 + case_parser = subparsers.add_parser("case", help="更改大小写") + case_parser.add_argument("case", choices=["lower", "upper", "title"], help="大小写类型") + + # 撤销命令 + undo_parser = subparsers.add_parser("undo", help="撤销最后一次重命名") + + args = parser.parse_args() + + # 创建重命名工具实例 + renamer = FileRenamer() + + # 加载历史记录 + renamer.load_history() + + # 执行相应命令 + if args.command == "undo": + renamer.undo_last_rename() + elif args.command == "replace": + count = renamer.simple_rename(args.directory, args.search, args.replace, args.preview) + print(f"{'预览' if args.preview else '重命名'}了 {count} 个文件") + elif args.command == "regex": + count = renamer.regex_rename(args.directory, args.pattern, args.replace, args.preview) + print(f"{'预览' if args.preview else '重命名'}了 {count} 个文件") + elif args.command == "prefix": + count = renamer.add_prefix(args.directory, args.prefix, args.preview) + print(f"{'预览' if args.preview else '重命名'}了 {count} 个文件") + elif args.command == "suffix": + count = renamer.add_suffix(args.directory, args.suffix, args.preview) + print(f"{'预览' if args.preview else '重命名'}了 {count} 个文件") + elif args.command == "enumerate": + count = renamer.enumerate_files(args.directory, args.prefix, args.start, args.digits, args.preview) + print(f"{'预览' if args.preview else '重命名'}了 {count} 个文件") + elif args.command == "case": + count = renamer.change_case(args.directory, args.case, args.preview) + print(f"{'预览' if args.preview else '重命名'}了 {count} 个文件") + else: + parser.print_help() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/gui_renamer.py b/gui_renamer.py new file mode 100644 index 0000000..7b971c3 --- /dev/null +++ b/gui_renamer.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +图形界面文件重命名工具 +""" + +import tkinter as tk +from tkinter import ttk, filedialog, messagebox, scrolledtext +import os +import threading +from file_renamer import FileRenamer + + +class GuiRenamer: + def __init__(self, root): + self.root = root + self.root.title("文件重命名工具") + self.root.geometry("800x600") + + # 创建重命名工具实例 + self.renamer = FileRenamer() + self.renamer.load_history() + + # 当前选择的目录 + self.current_directory = "" + + # 创建界面 + self.create_widgets() + + def create_widgets(self): + """创建界面组件""" + # 主框架 + main_frame = ttk.Frame(self.root, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # 配置网格权重 + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + main_frame.columnconfigure(1, weight=1) + main_frame.rowconfigure(4, weight=1) + + # 目录选择 + ttk.Label(main_frame, text="选择目录:").grid(row=0, column=0, sticky=tk.W, pady=5) + self.dir_var = tk.StringVar() + self.dir_entry = ttk.Entry(main_frame, textvariable=self.dir_var, width=50) + self.dir_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(5, 0), pady=5) + ttk.Button(main_frame, text="浏览", command=self.browse_directory).grid(row=0, column=2, padx=(5, 0), pady=5) + + # 分隔线 + ttk.Separator(main_frame, orient=tk.HORIZONTAL).grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10) + + # 重命名选项卡 + self.notebook = ttk.Notebook(main_frame) + self.notebook.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5) + + # 简单替换选项卡 + self.create_replace_tab() + + # 正则表达式选项卡 + self.create_regex_tab() + + # 添加前缀/后缀选项卡 + self.create_prefix_suffix_tab() + + # 序号选项卡 + self.create_enumerate_tab() + + # 大小写选项卡 + self.create_case_tab() + + # 预览按钮 + self.preview_btn = ttk.Button(main_frame, text="预览", command=self.preview_rename) + self.preview_btn.grid(row=3, column=1, sticky=tk.E, pady=10) + + # 执行按钮 + self.execute_btn = ttk.Button(main_frame, text="执行重命名", command=self.execute_rename) + self.execute_btn.grid(row=3, column=2, padx=(5, 0), pady=10) + + # 结果显示区域 + result_frame = ttk.LabelFrame(main_frame, text="结果", padding="5") + result_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5) + result_frame.columnconfigure(0, weight=1) + result_frame.rowconfigure(0, weight=1) + + self.result_text = scrolledtext.ScrolledText(result_frame, wrap=tk.WORD, height=15) + self.result_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # 撤销按钮 + self.undo_btn = ttk.Button(main_frame, text="撤销上次操作", command=self.undo_last_rename) + self.undo_btn.grid(row=5, column=0, pady=10) + + # 清空结果按钮 + ttk.Button(main_frame, text="清空结果", command=self.clear_result).grid(row=5, column=2, pady=10) + + def create_replace_tab(self): + """创建简单替换选项卡""" + frame = ttk.Frame(self.notebook, padding="10") + frame.columnconfigure(1, weight=1) + + ttk.Label(frame, text="查找:").grid(row=0, column=0, sticky=tk.W, pady=5) + self.replace_search_var = tk.StringVar() + ttk.Entry(frame, textvariable=self.replace_search_var, width=30).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(5, 0), pady=5) + + ttk.Label(frame, text="替换为:").grid(row=1, column=0, sticky=tk.W, pady=5) + self.replace_replace_var = tk.StringVar() + ttk.Entry(frame, textvariable=self.replace_replace_var, width=30).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(5, 0), pady=5) + + self.notebook.add(frame, text="简单替换") + self.replace_frame = frame + + def create_regex_tab(self): + """创建正则表达式选项卡""" + frame = ttk.Frame(self.notebook, padding="10") + frame.columnconfigure(1, weight=1) + + ttk.Label(frame, text="正则表达式:").grid(row=0, column=0, sticky=tk.W, pady=5) + self.regex_pattern_var = tk.StringVar() + ttk.Entry(frame, textvariable=self.regex_pattern_var, width=30).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(5, 0), pady=5) + + ttk.Label(frame, text="替换为:").grid(row=1, column=0, sticky=tk.W, pady=5) + self.regex_replace_var = tk.StringVar() + ttk.Entry(frame, textvariable=self.regex_replace_var, width=30).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(5, 0), pady=5) + + # 正则表达式说明 + help_text = "常用正则表达式:\n\\d+ 匹配数字\n[a-zA-Z]+ 匹配字母\n.+ 匹配任意字符\n^ 匹配开头\n$ 匹配结尾" + ttk.Label(frame, text=help_text, foreground="gray").grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=(10, 0)) + + self.notebook.add(frame, text="正则表达式") + self.regex_frame = frame + + def create_prefix_suffix_tab(self): + """创建前缀/后缀选项卡""" + frame = ttk.Frame(self.notebook, padding="10") + frame.columnconfigure(1, weight=1) + + ttk.Label(frame, text="前缀:").grid(row=0, column=0, sticky=tk.W, pady=5) + self.prefix_var = tk.StringVar() + ttk.Entry(frame, textvariable=self.prefix_var, width=30).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(5, 0), pady=5) + + ttk.Label(frame, text="后缀:").grid(row=1, column=0, sticky=tk.W, pady=5) + self.suffix_var = tk.StringVar() + ttk.Entry(frame, textvariable=self.suffix_var, width=30).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(5, 0), pady=5) + + self.notebook.add(frame, text="前缀/后缀") + self.prefix_suffix_frame = frame + + def create_enumerate_tab(self): + """创建序号选项卡""" + frame = ttk.Frame(self.notebook, padding="10") + frame.columnconfigure(1, weight=1) + + ttk.Label(frame, text="前缀:").grid(row=0, column=0, sticky=tk.W, pady=5) + self.enum_prefix_var = tk.StringVar() + ttk.Entry(frame, textvariable=self.enum_prefix_var, width=30).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(5, 0), pady=5) + + ttk.Label(frame, text="起始数字:").grid(row=1, column=0, sticky=tk.W, pady=5) + self.enum_start_var = tk.StringVar(value="1") + ttk.Entry(frame, textvariable=self.enum_start_var, width=30).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(5, 0), pady=5) + + ttk.Label(frame, text="数字位数:").grid(row=2, column=0, sticky=tk.W, pady=5) + self.enum_digits_var = tk.StringVar(value="3") + ttk.Entry(frame, textvariable=self.enum_digits_var, width=30).grid(row=2, column=1, sticky=(tk.W, tk.E), padx=(5, 0), pady=5) + + self.notebook.add(frame, text="添加序号") + self.enumerate_frame = frame + + def create_case_tab(self): + """创建大小写选项卡""" + frame = ttk.Frame(self.notebook, padding="10") + + ttk.Label(frame, text="选择大小写格式:").grid(row=0, column=0, sticky=tk.W, pady=5) + + self.case_var = tk.StringVar(value="lower") + case_frame = ttk.Frame(frame) + case_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=5) + + ttk.Radiobutton(case_frame, text="小写 (lowercase)", variable=self.case_var, value="lower").pack(anchor=tk.W) + ttk.Radiobutton(case_frame, text="大写 (UPPERCASE)", variable=self.case_var, value="upper").pack(anchor=tk.W) + ttk.Radiobutton(case_frame, text="首字母大写 (Title Case)", variable=self.case_var, value="title").pack(anchor=tk.W) + + self.notebook.add(frame, text="大小写") + self.case_frame = frame + + def browse_directory(self): + """浏览目录""" + directory = filedialog.askdirectory() + if directory: + self.dir_var.set(directory) + self.current_directory = directory + + def preview_rename(self): + """预览重命名""" + self.perform_rename(preview=True) + + def execute_rename(self): + """执行重命名""" + self.perform_rename(preview=False) + + def perform_rename(self, preview=True): + """执行重命名操作""" + # 获取当前目录 + directory = self.dir_var.get().strip() + if not directory: + messagebox.showerror("错误", "请选择一个目录") + return + + if not os.path.exists(directory): + messagebox.showerror("错误", "选择的目录不存在") + return + + self.current_directory = directory + + # 获取当前选中的选项卡 + current_tab = self.notebook.index(self.notebook.select()) + + # 在新线程中执行重命名以避免界面冻结 + thread = threading.Thread(target=self._perform_rename_thread, args=(current_tab, preview)) + thread.daemon = True + thread.start() + + def _perform_rename_thread(self, tab_index, preview): + """在后台线程中执行重命名""" + try: + # 根据选项卡索引执行不同的重命名操作 + if tab_index == 0: # 简单替换 + search_pattern = self.replace_search_var.get() + replace_pattern = self.replace_replace_var.get() + count = self.renamer.simple_rename(self.current_directory, search_pattern, replace_pattern, preview) + + elif tab_index == 1: # 正则表达式 + regex_pattern = self.regex_pattern_var.get() + replace_pattern = self.regex_replace_var.get() + count = self.renamer.regex_rename(self.current_directory, regex_pattern, replace_pattern, preview) + + elif tab_index == 2: # 前缀/后缀 + prefix = self.prefix_var.get() + suffix = self.suffix_var.get() + count = 0 + if prefix: + count += self.renamer.add_prefix(self.current_directory, prefix, preview) + if suffix: + count += self.renamer.add_suffix(self.current_directory, suffix, preview) + + elif tab_index == 3: # 序号 + prefix = self.enum_prefix_var.get() + try: + start = int(self.enum_start_var.get() or "1") + digits = int(self.enum_digits_var.get() or "3") + count = self.renamer.enumerate_files(self.current_directory, prefix, start, digits, preview) + except ValueError: + self.show_result("错误: 起始数字或数字位数必须是整数") + return + + elif tab_index == 4: # 大小写 + case_type = self.case_var.get() + count = self.renamer.change_case(self.current_directory, case_type, preview) + + # 显示结果 + action = "预览" if preview else "重命名" + self.show_result(f"{action}完成,共处理了 {count} 个文件") + + except Exception as e: + self.show_result(f"操作失败: {str(e)}") + + def show_result(self, message): + """在结果区域显示信息""" + self.result_text.insert(tk.END, f"{message}\n") + self.result_text.see(tk.END) + + def clear_result(self): + """清空结果区域""" + self.result_text.delete(1.0, tk.END) + + def undo_last_rename(self): + """撤销上次重命名操作""" + try: + if self.renamer.undo_last_rename(): + self.show_result("成功撤销上次重命名操作") + else: + self.show_result("没有可撤销的操作") + except Exception as e: + self.show_result(f"撤销操作失败: {str(e)}") + + +def main(): + """主函数""" + root = tk.Tk() + app = GuiRenamer(root) + root.mainloop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_renamer.py b/test_renamer.py new file mode 100644 index 0000000..2643735 --- /dev/null +++ b/test_renamer.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +文件重命名工具测试脚本 +""" + +import os +import shutil +import tempfile +from file_renamer import FileRenamer + + +def create_test_files(directory): + """创建测试文件""" + test_files = [ + "test_file_1.txt", + "test_file_2.txt", + "document_2023-01-01.pdf", + "image_001.jpg", + "IMAGE_002.JPG", + "Report.DOCX" + ] + + for filename in test_files: + filepath = os.path.join(directory, filename) + with open(filepath, 'w', encoding='utf-8') as f: + f.write(f"这是测试文件 {filename} 的内容") + + +def test_simple_rename(renamer, test_dir): + """测试简单重命名功能""" + print("测试简单重命名功能...") + + # 预览模式 + count = renamer.simple_rename(test_dir, "test", "example", preview=True) + print(f"预览模式重命名了 {count} 个文件") + + # 实际重命名 + count = renamer.simple_rename(test_dir, "test", "example", preview=False) + print(f"实际重命名了 {count} 个文件") + + # 验证结果 + files = os.listdir(test_dir) + renamed_files = [f for f in files if f.startswith("example")] + print(f"重命名后的文件: {renamed_files}") + + +def test_regex_rename(renamer, test_dir): + """测试正则表达式重命名功能""" + print("\n测试正则表达式重命名功能...") + + # 预览模式 + count = renamer.regex_rename(test_dir, r"\d+", "#", preview=True) + print(f"预览模式重命名了 {count} 个文件") + + # 实际重命名 + count = renamer.regex_rename(test_dir, r"\d+", "#", preview=False) + print(f"实际重命名了 {count} 个文件") + + +def test_prefix_suffix(renamer, test_dir): + """测试前缀/后缀功能""" + print("\n测试前缀/后缀功能...") + + # 添加前缀 + count = renamer.add_prefix(test_dir, "PRE_", preview=False) + print(f"添加前缀重命名了 {count} 个文件") + + # 添加后缀 + count = renamer.add_suffix(test_dir, "_SUF", preview=False) + print(f"添加后缀重命名了 {count} 个文件") + + +def test_enumerate(renamer, test_dir): + """测试编号功能""" + print("\n测试编号功能...") + + count = renamer.enumerate_files(test_dir, prefix="FILE_", start_number=1, digits=3, preview=False) + print(f"编号重命名了 {count} 个文件") + + +def test_case(renamer, test_dir): + """测试大小写转换功能""" + print("\n测试大小写转换功能...") + + # 转换为小写 + count = renamer.change_case(test_dir, "lower", preview=False) + print(f"小写转换重命名了 {count} 个文件") + + # 转换为大写 + count = renamer.change_case(test_dir, "upper", preview=False) + print(f"大写转换重命名了 {count} 个文件") + + +def test_undo(renamer): + """测试撤销功能""" + print("\n测试撤销功能...") + + result = renamer.undo_last_rename() + if result: + print("成功撤销一次操作") + else: + print("撤销操作失败或无操作可撤销") + + +def main(): + """主测试函数""" + print("开始测试文件重命名工具...") + + # 创建临时测试目录 + test_dir = tempfile.mkdtemp(prefix="renamer_test_") + print(f"创建测试目录: {test_dir}") + + try: + # 创建测试文件 + create_test_files(test_dir) + print(f"创建测试文件: {os.listdir(test_dir)}") + + # 创建重命名工具实例 + renamer = FileRenamer(os.path.join(test_dir, "test_rename_log.txt")) + + # 运行各项测试 + test_simple_rename(renamer, test_dir) + test_regex_rename(renamer, test_dir) + test_prefix_suffix(renamer, test_dir) + test_enumerate(renamer, test_dir) + test_case(renamer, test_dir) + test_undo(renamer) + + print("\n测试完成!") + print(f"最终文件列表: {os.listdir(test_dir)}") + + except Exception as e: + print(f"测试过程中出现错误: {e}") + finally: + # 清理测试目录 + try: + shutil.rmtree(test_dir) + print(f"已清理测试目录: {test_dir}") + except Exception as e: + print(f"清理测试目录时出错: {e}") + + +if __name__ == "__main__": + main() \ No newline at end of file