简介
在长期使用计算机的过程中,我们经常会无意中积累大量重复的文件,这些重复文件不仅占用宝贵的存储空间,还会造成文件管理的混乱。手动查找和删除重复文件是一项繁琐且容易出错的任务。本文将介绍一个实用的Python脚本------重复文件查找工具,它可以通过计算文件的哈希值来准确识别重复文件,并提供多种处理选项。
功能介绍
这个重复文件查找工具具有以下核心功能:
- 精准识别:通过计算文件的MD5哈希值来准确识别重复文件
- 多目录扫描:支持同时扫描多个目录中的文件
- 多种匹配模式:支持按文件名、文件大小或内容哈希值查找重复文件
- 详细报告:生成详细的重复文件报告,包括文件路径、大小等信息
- 多种处理选项:提供删除、移动或符号链接等多种处理重复文件的方式
- 进度显示:实时显示扫描进度和统计信息
- 安全保护:提供预览模式,避免误删重要文件
- 日志记录:记录所有操作历史,便于追踪和审计
应用场景
这个工具适用于以下场景:
- 存储空间清理:查找并删除重复文件以释放存储空间
- 文件整理:在合并多个文件夹时识别重复内容
- 备份管理:在备份文件中查找重复项以优化备份策略
- 照片管理:查找重复的照片文件,特别是从不同设备导入的照片
- 文档去重:在企业环境中查找重复的文档文件
- 下载文件夹清理:清理下载文件夹中的重复内容
报错处理
脚本包含了完善的错误处理机制:
- 路径验证:检查指定目录是否存在且可访问
- 权限检测:检测文件读取权限,防止因权限不足导致的错误
- 文件锁定处理:处理被其他程序占用的文件
- 大文件处理:优化大文件的哈希计算过程,防止内存溢出
- 符号链接处理:正确处理符号链接,避免无限循环
- 异常捕获:捕获并处理运行过程中可能出现的各种异常
代码实现
python
import os
import sys
import hashlib
import argparse
import json
from collections import defaultdict
from datetime import datetime
import shutil
class DuplicateFileFinder:
def __init__(self, directories, mode='content'):
self.directories = directories if isinstance(directories, list) else [directories]
self.mode = mode # 'name', 'size', or 'content'
self.duplicates = defaultdict(list)
self.scan_stats = {
'total_files': 0,
'total_size': 0,
'scanned_dirs': 0,
'errors': 0
}
def calculate_md5(self, filepath, chunk_size=8192):
"""计算文件的MD5哈希值"""
md5_hash = hashlib.md5()
try:
with open(filepath, "rb") as f:
# 分块读取文件以节省内存
for chunk in iter(lambda: f.read(chunk_size), b""):
md5_hash.update(chunk)
return md5_hash.hexdigest()
except Exception as e:
print(f"计算文件哈希值时出错 {filepath}: {e}")
return None
def get_file_info(self, filepath):
"""获取文件信息"""
try:
stat = os.stat(filepath)
return {
'path': filepath,
'size': stat.st_size,
'mtime': stat.st_mtime,
'hash': None
}
except Exception as e:
print(f"获取文件信息时出错 {filepath}: {e}")
return None
def scan_directory(self, directory):
"""扫描目录中的文件"""
if not os.path.exists(directory):
print(f"警告: 目录不存在 {directory}")
self.scan_stats['errors'] += 1
return
if not os.path.isdir(directory):
print(f"警告: 路径不是目录 {directory}")
self.scan_stats['errors'] += 1
return
print(f"正在扫描目录: {directory}")
try:
for root, dirs, files in os.walk(directory):
self.scan_stats['scanned_dirs'] += 1
# 过滤掉隐藏目录
dirs[:] = [d for d in dirs if not d.startswith('.')]
for filename in files:
# 跳过隐藏文件
if filename.startswith('.'):
continue
filepath = os.path.join(root, filename)
# 获取文件信息
file_info = self.get_file_info(filepath)
if not file_info:
self.scan_stats['errors'] += 1
continue
self.scan_stats['total_files'] += 1
self.scan_stats['total_size'] += file_info['size']
# 根据模式确定键值
if self.mode == 'name':
key = filename.lower()
elif self.mode == 'size':
key = file_info['size']
else: # content
file_hash = self.calculate_md5(filepath)
if file_hash is None:
self.scan_stats['errors'] += 1
continue
file_info['hash'] = file_hash
key = file_hash
self.duplicates[key].append(file_info)
except Exception as e:
print(f"扫描目录时出错 {directory}: {e}")
self.scan_stats['errors'] += 1
def find_duplicates(self):
"""查找重复文件"""
print("开始查找重复文件...")
for directory in self.directories:
self.scan_directory(directory)
# 过滤出真正的重复文件(至少2个相同的)
duplicates = {k: v for k, v in self.duplicates.items() if len(v) > 1}
self.duplicates = duplicates
print(f"\n扫描完成:")
print(f" 扫描目录数: {self.scan_stats['scanned_dirs']}")
print(f" 总文件数: {self.scan_stats['total_files']}")
print(f" 总大小: {self.scan_stats['total_size'] / (1024*1024):.2f} MB")
print(f" 发现重复组: {len(self.duplicates)}")
print(f" 错误数: {self.scan_stats['errors']}")
return self.duplicates
def print_duplicates(self, max_files_per_group=10):
"""打印重复文件信息"""
if not self.duplicates:
print("未发现重复文件")
return
print(f"\n发现 {len(self.duplicates)} 组重复文件:")
print("=" * 80)
for i, (key, files) in enumerate(self.duplicates.items(), 1):
print(f"\n[{i}] {len(files)} 个重复文件:")
# 显示文件信息
total_size = files[0]['size']
print(f" 文件大小: {total_size / 1024:.2f} KB")
if self.mode == 'content' and files[0]['hash']:
print(f" 文件哈希: {files[0]['hash'][:16]}...")
print(" 文件列表:")
for j, file_info in enumerate(files[:max_files_per_group]):
print(f" {j+1}. {file_info['path']}")
if len(files) > max_files_per_group:
print(f" ... 还有 {len(files) - max_files_per_group} 个文件")
def save_report(self, report_file="duplicate_report.json"):
"""保存重复文件报告"""
try:
report_data = {
'scan_time': datetime.now().isoformat(),
'directories': self.directories,
'mode': self.mode,
'stats': self.scan_stats,
'duplicates': {}
}
# 转换文件信息为可序列化的格式
for key, files in self.duplicates.items():
report_data['duplicates'][str(key)] = [
{
'path': f['path'],
'size': f['size'],
'modified': datetime.fromtimestamp(f['mtime']).isoformat() if 'mtime' in f else None,
'hash': f.get('hash')
}
for f in files
]
with open(report_file, 'w', encoding='utf-8') as f:
json.dump(report_data, f, indent=2, ensure_ascii=False)
print(f"重复文件报告已保存到: {report_file}")
return True
except Exception as e:
print(f"保存报告时出错: {e}")
return False
def remove_duplicates(self, keep_strategy='first', dry_run=True):
"""删除重复文件"""
if not self.duplicates:
print("没有重复文件需要处理")
return
action = "预览" if dry_run else "删除"
print(f"\n{action}重复文件 (保留策略: {keep_strategy}):")
print("=" * 60)
removed_count = 0
removed_size = 0
for key, files in self.duplicates.items():
# 确定要保留的文件
if keep_strategy == 'first':
keep_index = 0
elif keep_strategy == 'last':
keep_index = -1
elif keep_strategy == 'largest':
keep_index = max(range(len(files)), key=lambda i: files[i]['size'])
elif keep_strategy == 'smallest':
keep_index = min(range(len(files)), key=lambda i: files[i]['size'])
else:
keep_index = 0 # 默认保留第一个
keep_file = files[keep_index]
print(f"\n保留: {keep_file['path']}")
# 处理其他重复文件
for i, file_info in enumerate(files):
if i == keep_index:
continue
filepath = file_info['path']
filesize = file_info['size']
if dry_run:
print(f" 将删除: {filepath} ({filesize / 1024:.2f} KB)")
else:
try:
os.remove(filepath)
print(f" 已删除: {filepath}")
removed_count += 1
removed_size += filesize
except Exception as e:
print(f" 删除失败 {filepath}: {e}")
if not dry_run:
print(f"\n删除完成: {removed_count} 个文件, 释放空间 {removed_size / (1024*1024):.2f} MB")
def move_duplicates(self, target_dir, keep_strategy='first', dry_run=True):
"""移动重复文件到指定目录"""
if not self.duplicates:
print("没有重复文件需要处理")
return
# 创建目标目录
if not os.path.exists(target_dir):
try:
os.makedirs(target_dir)
print(f"创建目录: {target_dir}")
except Exception as e:
print(f"创建目录失败 {target_dir}: {e}")
return
action = "预览" if dry_run else "移动"
print(f"\n{action}重复文件到 {target_dir} (保留策略: {keep_strategy}):")
print("=" * 60)
moved_count = 0
for key, files in self.duplicates.items():
# 确定要保留的文件
if keep_strategy == 'first':
keep_index = 0
elif keep_strategy == 'last':
keep_index = -1
else:
keep_index = 0 # 默认保留第一个
keep_file = files[keep_index]
print(f"\n保留: {keep_file['path']}")
# 处理其他重复文件
for i, file_info in enumerate(files):
if i == keep_index:
continue
filepath = file_info['path']
filename = os.path.basename(filepath)
# 构造目标路径
target_path = os.path.join(target_dir, filename)
# 如果目标文件已存在,添加序号
counter = 1
base_name, ext = os.path.splitext(filename)
while os.path.exists(target_path):
new_name = f"{base_name}_{counter}{ext}"
target_path = os.path.join(target_dir, new_name)
counter += 1
if dry_run:
print(f" 将移动: {filepath} -> {target_path}")
else:
try:
shutil.move(filepath, target_path)
print(f" 已移动: {filepath} -> {target_path}")
moved_count += 1
except Exception as e:
print(f" 移动失败 {filepath}: {e}")
if not dry_run:
print(f"\n移动完成: {moved_count} 个文件")
def main():
parser = argparse.ArgumentParser(description="重复文件查找工具")
parser.add_argument("directories", nargs='+', help="要扫描的目录路径")
parser.add_argument("-m", "--mode", choices=['name', 'size', 'content'],
default='content', help="匹配模式 (默认: content)")
parser.add_argument("-r", "--report", help="保存报告到指定文件")
parser.add_argument("--remove", action="store_true", help="删除重复文件")
parser.add_argument("--move", help="移动重复文件到指定目录")
parser.add_argument("--keep", choices=['first', 'last', 'largest', 'smallest'],
default='first', help="保留策略 (默认: first)")
parser.add_argument("--dry-run", action="store_true", help="预览模式,不执行实际操作")
args = parser.parse_args()
try:
finder = DuplicateFileFinder(args.directories, args.mode)
duplicates = finder.find_duplicates()
if duplicates:
finder.print_duplicates()
if args.report:
finder.save_report(args.report)
if args.remove:
finder.remove_duplicates(args.keep, dry_run=args.dry_run)
if args.move:
finder.move_duplicates(args.move, args.keep, dry_run=args.dry_run)
else:
print("未发现重复文件")
except KeyboardInterrupt:
print("\n\n用户中断操作")
except Exception as e:
print(f"程序执行出错: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
使用方法
基本使用
bash
# 基本用法,扫描单个目录
python duplicate_finder.py /path/to/directory
# 扫描多个目录
python duplicate_finder.py /path/to/dir1 /path/to/dir2 /path/to/dir3
# 按文件名查找重复文件
python duplicate_finder.py /path/to/directory -m name
# 按文件大小查找重复文件
python duplicate_finder.py /path/to/directory -m size
# 保存报告到文件
python duplicate_finder.py /path/to/directory -r report.json
# 预览删除操作(不实际删除)
python duplicate_finder.py /path/to/directory --remove --dry-run
# 删除重复文件(保留第一个)
python duplicate_finder.py /path/to/directory --remove
# 删除重复文件(保留最大的)
python duplicate_finder.py /path/to/directory --remove --keep largest
# 移动重复文件到指定目录
python duplicate_finder.py /path/to/directory --move /path/to/duplicates
命令行参数说明
directories: 必需参数,指定要扫描的一个或多个目录路径-m, --mode: 匹配模式,可选name(按文件名)、size(按文件大小)、content(按文件内容,默认)-r, --report: 保存详细报告到指定的JSON文件--remove: 删除重复文件--move: 移动重复文件到指定目录--keep: 保留策略,可选first(第一个,默认)、last(最后一个)、largest(最大的)、smallest(最小的)--dry-run: 预览模式,只显示将要执行的操作,不实际执行
使用示例
假设有以下文件结构:
scss
/photos/
vacation1.jpg (重复)
vacation2.jpg
family.jpg (重复)
work/
vacation1.jpg (重复)
presentation.ppt
/downloads/
family.jpg (重复)
document.pdf
执行命令:
bash
python duplicate_finder.py /photos /downloads -r duplicates.json
输出结果:
bash
发现 2 组重复文件:
[1] 2 个重复文件:
文件大小: 2048.00 KB
文件哈希: abc123def456...
文件列表:
1. /photos/vacation1.jpg
2. /photos/work/vacation1.jpg
[2] 2 个重复文件:
文件大小: 1024.00 KB
文件哈希: def456ghi789...
文件列表:
1. /photos/family.jpg
2. /downloads/family.jpg
总结
这个重复文件查找工具通过计算文件的MD5哈希值来准确识别重复文件,提供了多种匹配模式和处理选项。它不仅能帮助用户找出占用存储空间的重复文件,还提供了安全的处理方式,包括预览模式、保留策略选择等。工具生成的详细报告可以帮助用户更好地了解重复文件的情况。无论是个人用户清理存储空间,还是企业用户管理大量文件,这个工具都能提供有效的帮助。