最近收集了三千多份近 12GB 的零散文件,发现其中有许多重复的,多占用很多空间。想用 Python 清除重复文件,便在百度上搜了搜方法,记录在下。
最简便方法——利用官方库
在 Python 中有一个官方库—— filecmp
,库里有一个函数:cmp()
,就是用来对文件进行比较的,我们可以使用它来操作。函数的使用方法如下:
1 2
| import filecmp filecmp.cmp(f1, f2, shallow=True)
|
它包含了三个参数,其中前两个参数表示的是需要比较的两个文件的路径,shallow 默认的值是 True ,是只比较两个文件的元数据,包括创建的时间、大小,如果为 False 的时候,表示在对比文件的时候,还需要对文件的内容进行对比。
我把源代码扒下来了,以下是对源代码的详解(英文注释已翻译):
点击展开代码 >folded1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| import stat import os
_cache = {}
BUFSIZE = 8*1024
def clear_cache(): """清除 filecmp 缓存""" _cache.clear()
def _sig(st): return stat.S_IFMT(st.st_mode), st.st_size, st.st_mtime
def _do_cmp(f1, f2): bufsize = BUFSIZE with open(f1, 'rb') as fp1, open(f2, 'rb') as fp2: while True: b1 = fp1.read(bufsize) b2 = fp2.read(bufsize) if b1 != b2: return False if not b1: return True
def cmp(f1, f2, shallow=True): """ 比较两个文件。 参数: f1 —— 第一个文件名 f2 —— 第二个文件名 shallow —— 只需检查stat签名(不读取文件)。默认为True。 返回值: 如果文件相同,则为True,否则为False。 此函数将缓存用于过去的比较和结果,如果缓存项的统计信息发生更改,则缓存项将无效。 可以通过调用 clear_cache() 来清除缓存。 """
s1 = _sig(os.stat(f1)) s2 = _sig(os.stat(f2))
if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG: return False if shallow and s1 == s2: return True if s1[1] != s2[1]: return False
outcome = _cache.get((f1, f2, s1, s2)) if outcome is None: outcome = _do_cmp(f1, f2) if len(_cache) > 100: clear_cache() _cache[f1, f2, s1, s2] = outcome return outcome
|
进阶自制方法
我的思路是:比较两文件的大小、MD5 值。后来担心文件对比不精确,有新增了比较 SHA1 值。
环境介绍:
操作系统版本:Windows 7
使用软件:PyCharm Community Edition 2022.1
Python 版本:3.8.10
点击展开代码 >folded1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
import hashlib
def getSmallFileMD5(fileName): with open(fileName, 'rb') as f: data = f.read() file_md5 = hashlib.md5(data).hexdigest() return file_md5
def getBigFileMD5(fileName): m = hashlib.md5() with open(fileName, 'rb') as fobj: while True: data = fobj.read(4096) if not data: break m.update(data) return m.hexdigest()
def getFileSHA1(fileName): with open(fileName, 'rb') as f: data = f.read() file_sha1 = hashlib.sha1(data).hexdigest() return file_sha1
if __name__ == '__main__': print(getSmallFileMD5('test.zip')) print(getBigFileMD5('test.zip')) print(getFileSHA1('test.zip'))
|
其中,需要用到 hashlib
库来获取文件的 MD5 和 SHA1 值。我还未深入了解它们的算法,感兴趣的同志们可以自行参阅 官方文档 。
在同一目录下新建 main.py
点击展开代码 >folded1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
import os from os.path import isfile, join import py_hash
_PATH_ = 'D:\\'
fileList = [f for f in os.listdir(_PATH_) if isfile(join(_PATH_, f))]
file_size_md5 = {}
for file in fileList: filePath = _PATH_ + '\\' + file
fsize = os.stat(filePath).st_size fmd5 = py_hash.getSmallFileMD5(filePath) fsha1 = py_hash.getFileSHA1(filePath)
file_size_md5[file] = [fsize, fmd5, fsha1]
while len(fileList) != 1: fileName = fileList[0] for otherFlie in fileList: if fileName != otherFlie: if file_size_md5[fileName] == file_size_md5[otherFlie]: print(f'Found: <{fileName}> & <{otherFlie}>\nRemove: {otherFlie}') otherFliePath = _PATH_ + '\\' + otherFlie if os.path.exists(otherFliePath): os.remove(otherFliePath) else: print(f'Not Found The File <{otherFlie}> !') fileList.remove(fileName) else: print('Not Found!')
|
以上这一段代码的关键在于熟练运用 os 库,获取文件名和文件字节大小。
小总结
其实,两种方法本质上是一致的。求 MD5 值和 SHA1 值时也需要分块读取文件,用特殊的算法产生一段哈希值。总的来说,还是直接使用 filecmp.cmp(f1, f2, shallow=False)
比较方便,精确度与方法二几乎无差别(当参数 shallow 为 False 时)。如有疑问或错误之处,欢迎来评论区和我聊聊!