422 lines
16 KiB
Python
422 lines
16 KiB
Python
#!/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() |