first commit

This commit is contained in:
2025-12-14 22:21:51 +08:00
commit f32c1653f0
6 changed files with 1194 additions and 0 deletions

155
.gitignore vendored Normal file
View File

@@ -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

114
README.md Normal file
View File

@@ -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`
可以通过历史记录文件查看所有重命名操作,并在需要时手动恢复文件名。

63
example_usage.py Normal file
View File

@@ -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()

422
file_renamer.py Normal file
View File

@@ -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()

294
gui_renamer.py Normal file
View File

@@ -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()

146
test_renamer.py Normal file
View File

@@ -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()