基于Python編寫一個(gè)重復(fù)文件查找工具
簡(jiǎn)介
在長(zhǎng)期使用計(jì)算機(jī)的過程中,我們經(jīng)常會(huì)無(wú)意中積累大量重復(fù)的文件,這些重復(fù)文件不僅占用寶貴的存儲(chǔ)空間,還會(huì)造成文件管理的混亂。手動(dòng)查找和刪除重復(fù)文件是一項(xiàng)繁瑣且容易出錯(cuò)的任務(wù)。本文將介紹一個(gè)實(shí)用的Python腳本——重復(fù)文件查找工具,它可以通過計(jì)算文件的哈希值來準(zhǔn)確識(shí)別重復(fù)文件,并提供多種處理選項(xiàng)。
功能介紹
這個(gè)重復(fù)文件查找工具具有以下核心功能:
- 精準(zhǔn)識(shí)別:通過計(jì)算文件的MD5哈希值來準(zhǔn)確識(shí)別重復(fù)文件
- 多目錄掃描:支持同時(shí)掃描多個(gè)目錄中的文件
- 多種匹配模式:支持按文件名、文件大小或內(nèi)容哈希值查找重復(fù)文件
- 詳細(xì)報(bào)告:生成詳細(xì)的重復(fù)文件報(bào)告,包括文件路徑、大小等信息
- 多種處理選項(xiàng):提供刪除、移動(dòng)或符號(hào)鏈接等多種處理重復(fù)文件的方式
- 進(jìn)度顯示:實(shí)時(shí)顯示掃描進(jìn)度和統(tǒng)計(jì)信息
- 安全保護(hù):提供預(yù)覽模式,避免誤刪重要文件
- 日志記錄:記錄所有操作歷史,便于追蹤和審計(jì)
應(yīng)用場(chǎng)景
這個(gè)工具適用于以下場(chǎng)景:
- 存儲(chǔ)空間清理:查找并刪除重復(fù)文件以釋放存儲(chǔ)空間
- 文件整理:在合并多個(gè)文件夾時(shí)識(shí)別重復(fù)內(nèi)容
- 備份管理:在備份文件中查找重復(fù)項(xiàng)以優(yōu)化備份策略
- 照片管理:查找重復(fù)的照片文件,特別是從不同設(shè)備導(dǎo)入的照片
- 文檔去重:在企業(yè)環(huán)境中查找重復(fù)的文檔文件
- 下載文件夾清理:清理下載文件夾中的重復(fù)內(nèi)容
報(bào)錯(cuò)處理
腳本包含了完善的錯(cuò)誤處理機(jī)制:
- 路徑驗(yàn)證:檢查指定目錄是否存在且可訪問
- 權(quán)限檢測(cè):檢測(cè)文件讀取權(quán)限,防止因權(quán)限不足導(dǎo)致的錯(cuò)誤
- 文件鎖定處理:處理被其他程序占用的文件
- 大文件處理:優(yōu)化大文件的哈希計(jì)算過程,防止內(nèi)存溢出
- 符號(hào)鏈接處理:正確處理符號(hào)鏈接,避免無(wú)限循環(huán)
- 異常捕獲:捕獲并處理運(yùn)行過程中可能出現(xiàn)的各種異常
代碼實(shí)現(xiàn)
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):
"""計(jì)算文件的MD5哈希值"""
md5_hash = hashlib.md5()
try:
with open(filepath, "rb") as f:
# 分塊讀取文件以節(jié)省內(nèi)存
for chunk in iter(lambda: f.read(chunk_size), b""):
md5_hash.update(chunk)
return md5_hash.hexdigest()
except Exception as e:
print(f"計(jì)算文件哈希值時(shí)出錯(cuò) {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"獲取文件信息時(shí)出錯(cuò) {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']
# 根據(jù)模式確定鍵值
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"掃描目錄時(shí)出錯(cuò) {directory}: {e}")
self.scan_stats['errors'] += 1
def find_duplicates(self):
"""查找重復(fù)文件"""
print("開始查找重復(fù)文件...")
for directory in self.directories:
self.scan_directory(directory)
# 過濾出真正的重復(fù)文件(至少2個(gè)相同的)
duplicates = {k: v for k, v in self.duplicates.items() if len(v) > 1}
self.duplicates = duplicates
print(f"\n掃描完成:")
print(f" 掃描目錄數(shù): {self.scan_stats['scanned_dirs']}")
print(f" 總文件數(shù): {self.scan_stats['total_files']}")
print(f" 總大小: {self.scan_stats['total_size'] / (1024*1024):.2f} MB")
print(f" 發(fā)現(xiàn)重復(fù)組: {len(self.duplicates)}")
print(f" 錯(cuò)誤數(shù): {self.scan_stats['errors']}")
return self.duplicates
def print_duplicates(self, max_files_per_group=10):
"""打印重復(fù)文件信息"""
if not self.duplicates:
print("未發(fā)現(xiàn)重復(fù)文件")
return
print(f"\n發(fā)現(xiàn) {len(self.duplicates)} 組重復(fù)文件:")
print("=" * 80)
for i, (key, files) in enumerate(self.duplicates.items(), 1):
print(f"\n[{i}] {len(files)} 個(gè)重復(fù)文件:")
# 顯示文件信息
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} 個(gè)文件")
def save_report(self, report_file="duplicate_report.json"):
"""保存重復(fù)文件報(bào)告"""
try:
report_data = {
'scan_time': datetime.now().isoformat(),
'directories': self.directories,
'mode': self.mode,
'stats': self.scan_stats,
'duplicates': {}
}
# 轉(zhuǎn)換文件信息為可序列化的格式
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"重復(fù)文件報(bào)告已保存到: {report_file}")
return True
except Exception as e:
print(f"保存報(bào)告時(shí)出錯(cuò): {e}")
return False
def remove_duplicates(self, keep_strategy='first', dry_run=True):
"""刪除重復(fù)文件"""
if not self.duplicates:
print("沒有重復(fù)文件需要處理")
return
action = "預(yù)覽" if dry_run else "刪除"
print(f"\n{action}重復(fù)文件 (保留策略: {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 # 默認(rèn)保留第一個(gè)
keep_file = files[keep_index]
print(f"\n保留: {keep_file['path']}")
# 處理其他重復(fù)文件
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} 個(gè)文件, 釋放空間 {removed_size / (1024*1024):.2f} MB")
def move_duplicates(self, target_dir, keep_strategy='first', dry_run=True):
"""移動(dòng)重復(fù)文件到指定目錄"""
if not self.duplicates:
print("沒有重復(fù)文件需要處理")
return
# 創(chuàng)建目標(biāo)目錄
if not os.path.exists(target_dir):
try:
os.makedirs(target_dir)
print(f"創(chuàng)建目錄: {target_dir}")
except Exception as e:
print(f"創(chuàng)建目錄失敗 {target_dir}: {e}")
return
action = "預(yù)覽" if dry_run else "移動(dòng)"
print(f"\n{action}重復(fù)文件到 {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 # 默認(rèn)保留第一個(gè)
keep_file = files[keep_index]
print(f"\n保留: {keep_file['path']}")
# 處理其他重復(fù)文件
for i, file_info in enumerate(files):
if i == keep_index:
continue
filepath = file_info['path']
filename = os.path.basename(filepath)
# 構(gòu)造目標(biāo)路徑
target_path = os.path.join(target_dir, filename)
# 如果目標(biāo)文件已存在,添加序號(hào)
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" 將移動(dòng): {filepath} -> {target_path}")
else:
try:
shutil.move(filepath, target_path)
print(f" 已移動(dòng): {filepath} -> {target_path}")
moved_count += 1
except Exception as e:
print(f" 移動(dòng)失敗 {filepath}: {e}")
if not dry_run:
print(f"\n移動(dòng)完成: {moved_count} 個(gè)文件")
def main():
parser = argparse.ArgumentParser(description="重復(fù)文件查找工具")
parser.add_argument("directories", nargs='+', help="要掃描的目錄路徑")
parser.add_argument("-m", "--mode", choices=['name', 'size', 'content'],
default='content', help="匹配模式 (默認(rèn): content)")
parser.add_argument("-r", "--report", help="保存報(bào)告到指定文件")
parser.add_argument("--remove", action="store_true", help="刪除重復(fù)文件")
parser.add_argument("--move", help="移動(dòng)重復(fù)文件到指定目錄")
parser.add_argument("--keep", choices=['first', 'last', 'largest', 'smallest'],
default='first', help="保留策略 (默認(rèn): first)")
parser.add_argument("--dry-run", action="store_true", help="預(yù)覽模式,不執(zhí)行實(shí)際操作")
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("未發(fā)現(xiàn)重復(fù)文件")
except KeyboardInterrupt:
print("\n\n用戶中斷操作")
except Exception as e:
print(f"程序執(zhí)行出錯(cuò): {e}")
sys.exit(1)
if __name__ == "__main__":
main()
使用方法
基本使用
# 基本用法,掃描單個(gè)目錄 python duplicate_finder.py /path/to/directory # 掃描多個(gè)目錄 python duplicate_finder.py /path/to/dir1 /path/to/dir2 /path/to/dir3 # 按文件名查找重復(fù)文件 python duplicate_finder.py /path/to/directory -m name # 按文件大小查找重復(fù)文件 python duplicate_finder.py /path/to/directory -m size # 保存報(bào)告到文件 python duplicate_finder.py /path/to/directory -r report.json # 預(yù)覽刪除操作(不實(shí)際刪除) python duplicate_finder.py /path/to/directory --remove --dry-run # 刪除重復(fù)文件(保留第一個(gè)) python duplicate_finder.py /path/to/directory --remove # 刪除重復(fù)文件(保留最大的) python duplicate_finder.py /path/to/directory --remove --keep largest # 移動(dòng)重復(fù)文件到指定目錄 python duplicate_finder.py /path/to/directory --move /path/to/duplicates
命令行參數(shù)說明
directories: 必需參數(shù),指定要掃描的一個(gè)或多個(gè)目錄路徑-m, --mode: 匹配模式,可選name(按文件名)、size(按文件大小)、content(按文件內(nèi)容,默認(rèn))-r, --report: 保存詳細(xì)報(bào)告到指定的JSON文件--remove: 刪除重復(fù)文件--move: 移動(dòng)重復(fù)文件到指定目錄--keep: 保留策略,可選first(第一個(gè),默認(rèn))、last(最后一個(gè))、largest(最大的)、smallest(最小的)--dry-run: 預(yù)覽模式,只顯示將要執(zhí)行的操作,不實(shí)際執(zhí)行
使用示例
假設(shè)有以下文件結(jié)構(gòu):
/photos/
vacation1.jpg (重復(fù))
vacation2.jpg
family.jpg (重復(fù))
work/
vacation1.jpg (重復(fù))
presentation.ppt
/downloads/
family.jpg (重復(fù))
document.pdf
執(zhí)行命令:
python duplicate_finder.py /photos /downloads -r duplicates.json
輸出結(jié)果:
發(fā)現(xiàn) 2 組重復(fù)文件:
[1] 2 個(gè)重復(fù)文件:
文件大小: 2048.00 KB
文件哈希: abc123def456...
文件列表:
1. /photos/vacation1.jpg
2. /photos/work/vacation1.jpg
[2] 2 個(gè)重復(fù)文件:
文件大小: 1024.00 KB
文件哈希: def456ghi789...
文件列表:
1. /photos/family.jpg
2. /downloads/family.jpg
總結(jié)
這個(gè)重復(fù)文件查找工具通過計(jì)算文件的MD5哈希值來準(zhǔn)確識(shí)別重復(fù)文件,提供了多種匹配模式和處理選項(xiàng)。它不僅能幫助用戶找出占用存儲(chǔ)空間的重復(fù)文件,還提供了安全的處理方式,包括預(yù)覽模式、保留策略選擇等。工具生成的詳細(xì)報(bào)告可以幫助用戶更好地了解重復(fù)文件的情況。無(wú)論是個(gè)人用戶清理存儲(chǔ)空間,還是企業(yè)用戶管理大量文件,這個(gè)工具都能提供有效的幫助。
以上就是基于Python編寫一個(gè)重復(fù)文件查找工具的詳細(xì)內(nèi)容,更多關(guān)于Python查找重復(fù)文件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
tensorflow實(shí)現(xiàn)簡(jiǎn)單的卷積網(wǎng)絡(luò)
這篇文章主要為大家詳細(xì)介紹了tensorflow實(shí)現(xiàn)簡(jiǎn)單的卷積網(wǎng)絡(luò),使用的數(shù)據(jù)集是MNIST,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05
python執(zhí)行shell獲取硬件參數(shù)寫入mysql的方法
這篇文章主要介紹了python執(zhí)行shell獲取硬件參數(shù)寫入mysql的方法,可實(shí)現(xiàn)對(duì)服務(wù)器硬件信息的讀取及寫入數(shù)據(jù)庫(kù)的功能,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2014-12-12
python使用pika庫(kù)調(diào)用rabbitmq交換機(jī)模式詳解
這篇文章主要介紹了python使用pika庫(kù)調(diào)用rabbitmq交換機(jī)模式詳解,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,感興趣的小伙伴可以參考一下2022-08-08
使用scipy.optimize的fsolve,root函數(shù)求解非線性方程問題
這篇文章主要介紹了使用scipy.optimize的fsolve,root函數(shù)求解非線性方程問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12
Keras:Unet網(wǎng)絡(luò)實(shí)現(xiàn)多類語(yǔ)義分割方式
本文主要利用U-Net網(wǎng)絡(luò)結(jié)構(gòu)實(shí)現(xiàn)了多類的語(yǔ)義分割,并展示了部分測(cè)試效果,希望對(duì)你有用!2020-06-06
一個(gè)基于flask的web應(yīng)用誕生 使用模板引擎和表單插件(2)
一個(gè)基于flask的web應(yīng)用誕生第二篇,這篇文章主要介紹了如何使用jinja2模板引擎和wtf表單插件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04

