first commit
This commit is contained in:
422
file_renamer.py
Normal file
422
file_renamer.py
Normal 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()
|
||||
Reference in New Issue
Block a user