Files
renamer/file_renamer.py
2025-12-14 22:21:51 +08:00

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