orc-order-v2/backup/v1_backup_20250502190248/clean_files.py
2025-05-02 19:05:42 +08:00

587 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
文件清理工具
-----------
用于清理输入/输出目录中的旧文件,支持按天数和文件名模式进行清理。
默认情况下会清理input目录下的所有图片文件和output目录下的Excel文件。
"""
import os
import re
import sys
import logging
import argparse
from datetime import datetime, timedelta
from pathlib import Path
import time
import glob
# 配置日志
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, 'clean_files.log')
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_file, encoding='utf-8'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
class FileCleaner:
"""文件清理工具类"""
def __init__(self, input_dir="input", output_dir="output"):
"""初始化清理工具"""
self.input_dir = input_dir
self.output_dir = output_dir
self.logs_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
# 确保目录存在
for directory in [self.input_dir, self.output_dir, self.logs_dir]:
os.makedirs(directory, exist_ok=True)
logger.info(f"确保目录存在: {directory}")
def get_file_stats(self, directory):
"""获取目录的文件统计信息"""
if not os.path.exists(directory):
logger.warning(f"目录不存在: {directory}")
return {}
stats = {
'total_files': 0,
'total_size': 0,
'oldest_file': None,
'newest_file': None,
'file_types': {},
'files_by_age': {
'1_day': 0,
'7_days': 0,
'30_days': 0,
'older': 0
}
}
now = datetime.now()
one_day_ago = now - timedelta(days=1)
seven_days_ago = now - timedelta(days=7)
thirty_days_ago = now - timedelta(days=30)
for root, _, files in os.walk(directory):
for file in files:
file_path = os.path.join(root, file)
# 跳过临时文件
if file.startswith('~$') or file.startswith('.'):
continue
# 文件信息
try:
file_stats = os.stat(file_path)
file_size = file_stats.st_size
mod_time = datetime.fromtimestamp(file_stats.st_mtime)
# 更新统计信息
stats['total_files'] += 1
stats['total_size'] += file_size
# 更新最旧和最新文件
if stats['oldest_file'] is None or mod_time < stats['oldest_file'][1]:
stats['oldest_file'] = (file_path, mod_time)
if stats['newest_file'] is None or mod_time > stats['newest_file'][1]:
stats['newest_file'] = (file_path, mod_time)
# 按文件类型统计
ext = os.path.splitext(file)[1].lower()
if ext in stats['file_types']:
stats['file_types'][ext]['count'] += 1
stats['file_types'][ext]['size'] += file_size
else:
stats['file_types'][ext] = {'count': 1, 'size': file_size}
# 按年龄统计
if mod_time > one_day_ago:
stats['files_by_age']['1_day'] += 1
elif mod_time > seven_days_ago:
stats['files_by_age']['7_days'] += 1
elif mod_time > thirty_days_ago:
stats['files_by_age']['30_days'] += 1
else:
stats['files_by_age']['older'] += 1
except Exception as e:
logger.error(f"处理文件时出错 {file_path}: {e}")
return stats
def print_stats(self):
"""打印文件统计信息"""
# 输入目录统计
input_stats = self.get_file_stats(self.input_dir)
output_stats = self.get_file_stats(self.output_dir)
print("\n===== 文件统计信息 =====")
# 打印输入目录统计
if input_stats:
print(f"\n输入目录 ({self.input_dir}):")
print(f" 总文件数: {input_stats['total_files']}")
print(f" 总大小: {self._format_size(input_stats['total_size'])}")
if input_stats['oldest_file']:
oldest = input_stats['oldest_file']
print(f" 最旧文件: {os.path.basename(oldest[0])} ({oldest[1].strftime('%Y-%m-%d %H:%M:%S')})")
if input_stats['newest_file']:
newest = input_stats['newest_file']
print(f" 最新文件: {os.path.basename(newest[0])} ({newest[1].strftime('%Y-%m-%d %H:%M:%S')})")
print(" 文件年龄分布:")
print(f" 1天内: {input_stats['files_by_age']['1_day']}个文件")
print(f" 7天内(不含1天内): {input_stats['files_by_age']['7_days']}个文件")
print(f" 30天内(不含7天内): {input_stats['files_by_age']['30_days']}个文件")
print(f" 更旧: {input_stats['files_by_age']['older']}个文件")
print(" 文件类型分布:")
for ext, data in sorted(input_stats['file_types'].items(), key=lambda x: x[1]['count'], reverse=True):
print(f" {ext or '无扩展名'}: {data['count']}个文件, {self._format_size(data['size'])}")
# 打印输出目录统计
if output_stats:
print(f"\n输出目录 ({self.output_dir}):")
print(f" 总文件数: {output_stats['total_files']}")
print(f" 总大小: {self._format_size(output_stats['total_size'])}")
if output_stats['oldest_file']:
oldest = output_stats['oldest_file']
print(f" 最旧文件: {os.path.basename(oldest[0])} ({oldest[1].strftime('%Y-%m-%d %H:%M:%S')})")
if output_stats['newest_file']:
newest = output_stats['newest_file']
print(f" 最新文件: {os.path.basename(newest[0])} ({newest[1].strftime('%Y-%m-%d %H:%M:%S')})")
print(" 文件年龄分布:")
print(f" 1天内: {output_stats['files_by_age']['1_day']}个文件")
print(f" 7天内(不含1天内): {output_stats['files_by_age']['7_days']}个文件")
print(f" 30天内(不含7天内): {output_stats['files_by_age']['30_days']}个文件")
print(f" 更旧: {output_stats['files_by_age']['older']}个文件")
def _format_size(self, size_bytes):
"""格式化文件大小"""
if size_bytes < 1024:
return f"{size_bytes} 字节"
elif size_bytes < 1024 * 1024:
return f"{size_bytes/1024:.2f} KB"
elif size_bytes < 1024 * 1024 * 1024:
return f"{size_bytes/(1024*1024):.2f} MB"
else:
return f"{size_bytes/(1024*1024*1024):.2f} GB"
def clean_files(self, directory, days=None, pattern=None, extensions=None, exclude_patterns=None, force=False, test_mode=False):
"""
清理指定目录中的文件
参数:
directory (str): 要清理的目录
days (int): 保留的天数超过这个天数的文件将被清理None表示不考虑时间
pattern (str): 文件名匹配模式(正则表达式)
extensions (list): 要删除的文件扩展名列表,如['.jpg', '.xlsx']
exclude_patterns (list): 要排除的文件名模式列表
force (bool): 是否强制清理,不显示确认提示
test_mode (bool): 测试模式,只显示要删除的文件而不实际删除
返回:
tuple: (cleaned_count, cleaned_size) 清理的文件数量和总大小
"""
if not os.path.exists(directory):
logger.warning(f"目录不存在: {directory}")
return 0, 0
cutoff_date = None
if days is not None:
cutoff_date = datetime.now() - timedelta(days=days)
pattern_regex = re.compile(pattern) if pattern else None
files_to_clean = []
logger.info(f"扫描目录: {directory}")
# 查找需要清理的文件
for root, _, files in os.walk(directory):
for file in files:
file_path = os.path.join(root, file)
# 跳过临时文件
if file.startswith('~$') or file.startswith('.'):
continue
# 检查是否在排除列表中
if exclude_patterns and any(pattern in file for pattern in exclude_patterns):
logger.info(f"跳过文件: {file}")
continue
# 检查文件扩展名
if extensions and not any(file.lower().endswith(ext.lower()) for ext in extensions):
continue
# 检查修改时间
if cutoff_date:
try:
mod_time = datetime.fromtimestamp(os.path.getmtime(file_path))
if mod_time >= cutoff_date:
logger.debug(f"文件未超过保留天数: {file} - {mod_time.strftime('%Y-%m-%d %H:%M:%S')}")
continue
except Exception as e:
logger.error(f"检查文件时间时出错 {file_path}: {e}")
continue
# 检查是否匹配模式
if pattern_regex and not pattern_regex.search(file):
continue
try:
file_size = os.path.getsize(file_path)
files_to_clean.append((file_path, file_size))
logger.info(f"找到要清理的文件: {file_path}")
except Exception as e:
logger.error(f"获取文件大小时出错 {file_path}: {e}")
if not files_to_clean:
logger.info(f"没有找到需要清理的文件: {directory}")
return 0, 0
# 显示要清理的文件
total_size = sum(f[1] for f in files_to_clean)
print(f"\n找到 {len(files_to_clean)} 个文件要清理,总大小: {self._format_size(total_size)}")
if len(files_to_clean) > 10:
print("前10个文件:")
for file_path, size in files_to_clean[:10]:
print(f" {os.path.basename(file_path)} ({self._format_size(size)})")
print(f" ...以及其他 {len(files_to_clean) - 10} 个文件")
else:
for file_path, size in files_to_clean:
print(f" {os.path.basename(file_path)} ({self._format_size(size)})")
# 如果是测试模式,就不实际删除
if test_mode:
print("\n测试模式:不会实际删除文件。")
return len(files_to_clean), total_size
# 确认清理
if not force:
confirm = input(f"\n确定要清理这些文件吗?[y/N] ")
if confirm.lower() != 'y':
print("清理操作已取消。")
return 0, 0
# 执行清理
cleaned_count = 0
cleaned_size = 0
for file_path, size in files_to_clean:
try:
# 删除文件
try:
# 尝试检查文件是否被其他进程占用
if os.path.exists(file_path):
# 在Windows系统上可能需要先关闭可能打开的文件句柄
if sys.platform == 'win32':
try:
# 尝试重命名文件,如果被占用通常会失败
temp_path = file_path + '.temp'
os.rename(file_path, temp_path)
os.rename(temp_path, file_path)
except Exception as e:
logger.warning(f"文件可能被占用: {file_path}, 错误: {e}")
# 尝试关闭文件句柄仅Windows
try:
import ctypes
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
handle = kernel32.CreateFileW(file_path, 0x80000000, 0, None, 3, 0x80, None)
if handle != -1:
kernel32.CloseHandle(handle)
except Exception:
pass
# 使用Path对象删除文件
try:
Path(file_path).unlink(missing_ok=True)
logger.info(f"已删除文件: {file_path}")
cleaned_count += 1
cleaned_size += size
except Exception as e1:
# 如果Path.unlink失败尝试使用os.remove
try:
os.remove(file_path)
logger.info(f"使用os.remove删除文件: {file_path}")
cleaned_count += 1
cleaned_size += size
except Exception as e2:
logger.error(f"删除文件失败 {file_path}: {e1}, 再次尝试: {e2}")
else:
logger.warning(f"文件不存在或已被删除: {file_path}")
except Exception as e:
logger.error(f"删除文件时出错 {file_path}: {e}")
except Exception as e:
logger.error(f"处理文件时出错 {file_path}: {e}")
print(f"\n已清理 {cleaned_count} 个文件,总大小: {self._format_size(cleaned_size)}")
return cleaned_count, cleaned_size
def clean_image_files(self, force=False, test_mode=False):
"""清理输入目录中的图片文件"""
print(f"\n===== 清理输入目录图片文件 ({self.input_dir}) =====")
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
return self.clean_files(
self.input_dir,
days=None, # 不考虑天数,清理所有图片
extensions=image_extensions,
force=force,
test_mode=test_mode
)
def clean_excel_files(self, force=False, test_mode=False):
"""清理输出目录中的Excel文件"""
print(f"\n===== 清理输出目录Excel文件 ({self.output_dir}) =====")
excel_extensions = ['.xlsx', '.xls']
exclude_patterns = ['processed_files.json'] # 保留处理记录文件
return self.clean_files(
self.output_dir,
days=None, # 不考虑天数清理所有Excel
extensions=excel_extensions,
exclude_patterns=exclude_patterns,
force=force,
test_mode=test_mode
)
def clean_log_files(self, days=None, force=False, test_mode=False):
"""清理日志目录中的旧日志文件
参数:
days (int): 保留的天数超过这个天数的日志将被清理None表示清理所有日志
force (bool): 是否强制清理,不显示确认提示
test_mode (bool): 测试模式,只显示要删除的文件而不实际删除
"""
print(f"\n===== 清理日志文件 ({self.logs_dir}) =====")
log_extensions = ['.log']
# 排除当前正在使用的日志文件
current_log = os.path.basename(log_file)
logger.info(f"当前使用的日志文件: {current_log}")
result = self.clean_files(
self.logs_dir,
days=days, # 如果days=None清理所有日志文件
extensions=log_extensions,
exclude_patterns=[current_log], # 排除当前使用的日志文件
force=force,
test_mode=test_mode
)
return result
def clean_logs(self, days=7, force=False, test=False):
"""清理日志目录中的日志文件"""
try:
logs_dir = self.logs_dir
if not os.path.exists(logs_dir):
logger.warning(f"日志目录不存在: {logs_dir}")
return
cutoff_date = datetime.now() - timedelta(days=days)
files_to_delete = []
# 检查是否有活跃标记文件
active_files = set()
for marker_file in glob.glob(os.path.join(logs_dir, '*.active')):
active_log_name = os.path.basename(marker_file).replace('.active', '.log')
active_files.add(active_log_name)
logger.info(f"检测到活跃日志文件: {active_log_name}")
for file_path in glob.glob(os.path.join(logs_dir, '*.log*')):
file_name = os.path.basename(file_path)
# 跳过活跃的日志文件
if file_name in active_files:
logger.info(f"跳过活跃日志文件: {file_name}")
continue
mtime = os.path.getmtime(file_path)
if datetime.fromtimestamp(mtime) < cutoff_date:
files_to_delete.append(file_path)
if not files_to_delete:
logger.info("没有找到需要清理的日志文件")
return
logger.info(f"找到 {len(files_to_delete)} 个过期的日志文件")
for file_path in files_to_delete:
if test:
logger.info(f"测试模式 - 将删除: {os.path.basename(file_path)}")
else:
if not force:
response = input(f"是否删除日志文件 {os.path.basename(file_path)}? (y/n): ")
if response.lower() != 'y':
logger.info(f"已跳过 {os.path.basename(file_path)}")
continue
try:
os.remove(file_path)
logger.info(f"已删除日志文件: {os.path.basename(file_path)}")
except Exception as e:
logger.error(f"删除文件失败: {file_path}, 错误: {e}")
except Exception as e:
logger.error(f"清理日志文件时出错: {e}")
def clean_all_logs(self, force=False, test=False, except_current=True):
"""清理所有日志文件"""
try:
logs_dir = self.logs_dir
if not os.path.exists(logs_dir):
logger.warning(f"日志目录不存在: {logs_dir}")
return
# 检查是否有活跃标记文件
active_files = set()
for marker_file in glob.glob(os.path.join(logs_dir, '*.active')):
active_log_name = os.path.basename(marker_file).replace('.active', '.log')
active_files.add(active_log_name)
logger.info(f"检测到活跃日志文件: {active_log_name}")
files_to_delete = []
for file_path in glob.glob(os.path.join(logs_dir, '*.log*')):
file_name = os.path.basename(file_path)
# 跳过当前正在使用的日志文件
if except_current and file_name in active_files:
logger.info(f"保留活跃日志文件: {file_name}")
continue
files_to_delete.append(file_path)
if not files_to_delete:
logger.info("没有找到需要清理的日志文件")
return
logger.info(f"找到 {len(files_to_delete)} 个日志文件需要清理")
for file_path in files_to_delete:
if test:
logger.info(f"测试模式 - 将删除: {os.path.basename(file_path)}")
else:
if not force:
response = input(f"是否删除日志文件 {os.path.basename(file_path)}? (y/n): ")
if response.lower() != 'y':
logger.info(f"已跳过 {os.path.basename(file_path)}")
continue
try:
os.remove(file_path)
logger.info(f"已删除日志文件: {os.path.basename(file_path)}")
except Exception as e:
logger.error(f"删除文件失败: {file_path}, 错误: {e}")
except Exception as e:
logger.error(f"清理所有日志文件时出错: {e}")
def main():
"""主程序"""
parser = argparse.ArgumentParser(description='文件清理工具')
parser.add_argument('--stats', action='store_true', help='显示文件统计信息')
parser.add_argument('--clean-input', action='store_true', help='清理输入目录中超过指定天数的文件')
parser.add_argument('--clean-output', action='store_true', help='清理输出目录中超过指定天数的文件')
parser.add_argument('--clean-images', action='store_true', help='清理输入目录中的所有图片文件')
parser.add_argument('--clean-excel', action='store_true', help='清理输出目录中的所有Excel文件')
parser.add_argument('--clean-logs', action='store_true', help='清理日志目录中超过指定天数的日志文件')
parser.add_argument('--clean-all-logs', action='store_true', help='清理所有日志文件(除当前使用的)')
parser.add_argument('--days', type=int, default=30, help='保留的天数默认30天')
parser.add_argument('--log-days', type=int, default=7, help='保留的日志天数默认7天')
parser.add_argument('--pattern', type=str, help='文件名匹配模式(正则表达式)')
parser.add_argument('--force', action='store_true', help='强制清理,不显示确认提示')
parser.add_argument('--test', action='store_true', help='测试模式,只显示要删除的文件而不实际删除')
parser.add_argument('--input-dir', type=str, default='input', help='指定输入目录')
parser.add_argument('--output-dir', type=str, default='output', help='指定输出目录')
parser.add_argument('--help-only', action='store_true', help='只显示帮助信息,不执行任何操作')
parser.add_argument('--all', action='store_true', help='清理所有类型的文件(输入、输出和日志)')
args = parser.parse_args()
cleaner = FileCleaner(args.input_dir, args.output_dir)
# 显示统计信息
if args.stats:
cleaner.print_stats()
# 如果指定了--help-only只显示帮助信息
if args.help_only:
parser.print_help()
return
# 如果指定了--all清理所有类型的文件
if args.all:
cleaner.clean_image_files(args.force, args.test)
cleaner.clean_excel_files(args.force, args.test)
cleaner.clean_log_files(args.log_days, args.force, args.test)
cleaner.clean_all_logs(args.force, args.test)
return
# 清理输入目录中的图片文件
if args.clean_images or not any([args.stats, args.clean_input, args.clean_output,
args.clean_excel, args.clean_logs, args.clean_all_logs, args.help_only]):
cleaner.clean_image_files(args.force, args.test)
# 清理输出目录中的Excel文件
if args.clean_excel or not any([args.stats, args.clean_input, args.clean_output,
args.clean_images, args.clean_logs, args.clean_all_logs, args.help_only]):
cleaner.clean_excel_files(args.force, args.test)
# 清理日志文件(按天数)
if args.clean_logs:
cleaner.clean_log_files(args.log_days, args.force, args.test)
# 清理所有日志文件
if args.clean_all_logs:
cleaner.clean_all_logs(args.force, args.test)
# 清理输入目录(按天数)
if args.clean_input:
print(f"\n===== 清理输入目录 ({args.input_dir}) =====")
cleaner.clean_files(
args.input_dir,
days=args.days,
pattern=args.pattern,
force=args.force,
test_mode=args.test
)
# 清理输出目录(按天数)
if args.clean_output:
print(f"\n===== 清理输出目录 ({args.output_dir}) =====")
cleaner.clean_files(
args.output_dir,
days=args.days,
pattern=args.pattern,
force=args.force,
test_mode=args.test
)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n程序已被用户中断")
except Exception as e:
logger.error(f"程序运行出错: {e}", exc_info=True)
print(f"程序运行出错: {e}")
print("请查看日志文件了解详细信息")
sys.exit(0)