Python 文件去重

最近收集了三千多份近 12GB 的零散文件,发现其中有许多重复的,多占用很多空间。想用 Python 清除重复文件,便在百度上搜了搜方法,记录在下。

最简便方法——利用官方库

在 Python 中有一个官方库—— filecmp ,库里有一个函数:cmp() ,就是用来对文件进行比较的,我们可以使用它来操作。函数的使用方法如下:

1
2
import filecmp
filecmp.cmp(f1, f2, shallow=True)

它包含了三个参数,其中前两个参数表示的是需要比较的两个文件的路径,shallow 默认的值是 True ,是只比较两个文件的元数据,包括创建的时间、大小,如果为 False 的时候,表示在对比文件的时候,还需要对文件的内容进行对比。

我把源代码扒下来了,以下是对源代码的详解(英文注释已翻译):

点击展开代码 >folded
1
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):
# 返回文件 os.stat(f) 的返回文件的类型、文件的大小(以位为单位)、文件最后修改时间
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:
# 分块(大小为8*1024位)读取文件,比较各部分数据是否一致
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))

# 如果检查两个文件是否为 stat.S_IFREG(普通文件)。
if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG:
return False
# 如果 shallow 为 True ,则只比较文件的大小及文件最后修改时间,成立则返回 True
if shallow and s1 == s2:
return True
# 如果两文件大小不一致,返回 False
if s1[1] != s2[1]:
return False

# 获取(f1, f2, s1, s2)的值是否在缓存中。
# 如果有,获取值;如没有,则运行 _do_cmp(f1, f2) 获取值。
outcome = _cache.get((f1, f2, s1, s2))
if outcome is None:
outcome = _do_cmp(f1, f2)
if len(_cache) > 100: # 限制缓存的最大大小
clear_cache()
# 把数据添加到缓存,再次比较时无需运行 _do_cmp(f1, f2)
_cache[f1, f2, s1, s2] = outcome
return outcome

进阶自制方法

我的思路是:比较两文件的大小、MD5 值。后来担心文件对比不精确,有新增了比较 SHA1 值。

环境介绍:
操作系统版本:Windows 7
使用软件:PyCharm Community Edition 2022.1
Python 版本:3.8.10

点击展开代码 >folded
1
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
# 创建者:六月的风 JuneWind
# 文件名称:py_hash.py
# 创建时间:2022/10/15 星期六 13:39

import hashlib

# 获取文件的MD5值,适用于小文件
def getSmallFileMD5(fileName):
with open(fileName, 'rb') as f:
data = f.read()
file_md5 = hashlib.md5(data).hexdigest()
return file_md5

# 获取文件的MD5值,适用于较大文件
def getBigFileMD5(fileName):
m = hashlib.md5() # 创建 md5 对象
with open(fileName, 'rb') as fobj:
while True:
data = fobj.read(4096)
if not data:
break
m.update(data) # 更新 md5 对象
return m.hexdigest() # 返回 md5 对象

# 获取文件的SHA1值
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

点击展开代码 >folded
1
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
# 创建者:六月的风 JuneWind
# 文件名称:main.py
# 创建时间:2022/10/15 星期六 14:25

import os
from os.path import isfile, join
import py_hash

_PATH_ = 'D:\\'

# 获取目录 _PATH_ 中所有的文件
fileList = [f for f in os.listdir(_PATH_) if isfile(join(_PATH_, f))]

# 字典格式:{文件名: [字节大小, 文件MD5值], ...}
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]

# print(file_size_md5)
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 时)。如有疑问或错误之处,欢迎来评论区和我聊聊!

作者

JuneWind

发布于

2022-10-16

更新于

2023-03-06

许可协议

评论