通过hashlib查找重复文件?

时间:2013-09-10 16:33:36

标签: python file duplicates hashlib

我知道之前已经问过这个问题,我已经看到了一些答案,但这个问题更多的是关于我的代码和完成这项任务的最佳方法。

我想扫描一个目录,看看该目录中是否有任何重复项(通过检查MD5哈希)。以下是我的代码:

import sys
import os
import hashlib

fileSliceLimitation = 5000000 #bytes

# if the file is big, slice trick to avoid to load the whole file into RAM
def getFileHashMD5(filename):
     retval = 0;
     filesize = os.path.getsize(filename)

     if filesize > fileSliceLimitation:
        with open(filename, 'rb') as fh:
          m = hashlib.md5()
          while True:
            data = fh.read(8192)
            if not data:
                break
            m.update(data)
          retval = m.hexdigest()

     else:
        retval = hashlib.md5(open(filename, 'rb').read()).hexdigest()

     return retval

searchdirpath = raw_input("Type directory you wish to search: ")
print ""
print ""    
text_file = open('outPut.txt', 'w')

for dirname, dirnames, filenames in os.walk(searchdirpath):
    # print path to all filenames.
    for filename in filenames:
        fullname = os.path.join(dirname, filename)
        h_md5 = getFileHashMD5 (fullname)
        print h_md5 + " " + fullname
        text_file.write("\n" + h_md5 + " " + fullname)   

# close txt file
text_file.close()


print "\n\n\nReading outPut:"
text_file = open('outPut.txt', 'r')

myListOfHashes = text_file.read()

if h_md5 in myListOfHashes:
    print 'Match: ' + " " + fullname

这给了我以下输出:

Please type in directory you wish to search using above syntax: /Users/bubble/Desktop/aF

033808bb457f622b05096c2f7699857v /Users/bubble/Desktop/aF/.DS_Store
409d8c1727960fddb7c8b915a76ebd35 /Users/bubble/Desktop/aF/script copy.py
409d8c1727960fddb7c8b915a76ebd25 /Users/bubble/Desktop/aF/script.py
e9289295caefef66eaf3a4dffc4fe11c /Users/bubble/Desktop/aF/simpsons.mov

Reading outPut:
Match:  /Users/bubble/Desktop/aF/simpsons.mov

我的想法是:

1)扫描目录 2)将MD5哈希值+文件名写入文本文件 3)以只读方式打开文本文件 4)扫描目录AGAIN并检查文本文件...

我发现这不是一个好方法,它不起作用。 'match'只打印出最后处理过的文件。

如何让这个脚本实际找到重复项?有人能告诉我更好/更简单的方法来完成这项任务。

非常感谢您的帮助。对不起,这是一篇很长的帖子。

3 个答案:

答案 0 :(得分:5)

用于识别重复项的明显工具是哈希表。除非您使用非常大的数量的文件,否则您可以执行以下操作:

from collections import defaultdict

file_dict = defaultdict(list)
for filename in files:
    file_dict[get_file_hash(filename)].append(filename)

在此过程结束时,file_dict将包含每个唯一哈希的列表;当两个文件具有相同的哈希时,它们都将出现在该哈希的列表中。然后过滤dict,查找长于1的值列表,并比较文件以确保它们是相同的 - 如下所示:

for duplicates in file_dict.values():   # file_dict.itervalues() in Python 2
    if len(duplicates) > 1:
        # double-check reported duplicates and generate output

或者这个:

duplicates = [files for files in file_dict.values() if len(files) > 1]

get_file_hash可以使用MD5;或者它可以简单地获取文件的第一个和最后一个字节,正如Ramchandra Apte在上面的评论中所建议的那样;或者它可以简单地使用文件大小作为上面评论中提出的tdelaney。后两种策略中的每一种都更有可能产生误报。您可以将它们组合起来以降低误报率。

如果您正在使用非常大量文件,则可以使用更复杂的数据结构,例如Bloom Filter

答案 1 :(得分:3)

@senderle有一个很好的答案,但由于他提到我的解决方案会产生误报,我认为已经奠定了挑战,我最好展示一些代码。我减少了你的md5函数(它应该总是使用'​​fileSliceLimitation'的情况,并且应该不那么吝啬它的输入缓冲区),然后在执行md5s之前按大小预先过滤。

import sys
import os
import hashlib
from collections import defaultdict

searchdirpath = sys.argv[1]

size_map = defaultdict(list)

def getFileHashMD5(filename):
    m = hashlib.md5()
    with open(filename, 'rb', 1024*1024) as fh:
          while True:
            data = fh.read(1024*1024)
            if not data:
                break
            m.update(data)
    return m.hexdigest()

# group files by size
for dirname, dirnames, filenames in os.walk(searchdirpath):
    for filename in filenames:
        fullname = os.path.join(dirname, filename)
        size_map[os.stat(fullname).st_size].append(fullname)

# scan files of same size
for fullnames in size_map.itervalues():
    if len(fullnames) > 0:
        hash_map = defaultdict(list)
        for fullname in fullnames:
            hash_map[getFileHashMD5(fullname)].append(fullname)
        for fullnames in hash_map.itervalues():
            if len(fullnames) > 1:
                print "duplicates:"
                for fullname in fullnames:
                    print "   ", fullname

(编辑)

我将尝试在此处回答有关此实施的几个问题:

1)为什么(1024 * 1024)尺寸不是'5000000'

您的原始代码以8192(8 KiB)为增量读取,这对于现代系统而言非常小。你可以通过一次抓取更多来获得更好的性能。 1024 * 1024是1048576(1 MiB)字节,只是猜测一个合理的数字。至于为什么我以这种奇怪的方式写它,1000(十进制千字节)被人们所喜爱,但1024(二进制kibibyte)被计算机和文件系统所喜爱。我习惯于写some_number*1024所以很容易看出我正在提到1 KiB增量。 5000000也是一个合理的数字,但你应该考虑5 * 1024 * 1024(即5 MiB),这样你就能得到一些与文件系统很好地对齐的东西。

2)这一位究竟做了什么:size_map = defaultdict(list)

它创建了一个'defaultdict',它为常规的dict对象添加了功能。常规字典在由不存在的键索引时会引发KeyError异常。 defaultdict创建一个默认值,并将该键/值对添加到dict中。在我们的例子中,size_map[some_size]说“给我一些some_size的文件列表,如果没有,则创建一个新的空列表”。

size_map[os.stat(fullname).st_size].append(fullname)。这分解为:

stat = os.stat(fullname)
size = stat.st_size
filelist = size_map[size]    # this is the same as:
                             #    if size not in size_map:
                             #        size_map[size] = list()
                             #    filelist = size_map[size]
filelist.append(fullname)

3)sys.argv [1]我猜测sys.argv [1]只是让python py.py'filepath'参数工作(其中filepath是argv [1]?

是的,当你调用python脚本时,sys.argv [0]是脚本的名称,sys.argv [1:](arg 1及以下)是命令行中给出的任何其他参数。我使用sys.argv [1]作为在编写脚本时测试脚本的快速方法,您应该更改它以满足您的需求。

答案 2 :(得分:0)

您要做的第一件事是在循环浏览文件时将h_md5保存到列表中。 类似的东西:

h_md5=[]
循环浏览目录之前

。和

h_md5.append(getFileHashMD5(fullname))
循环中的

。现在你有一个哈希列表要与你的输出文件进行比较,而不是你在循环中做的最后一个哈希。

另外,显然,使用您当前的代码,您每次都会为每个文件找到一个匹配项,因为您将在列表中找到该特定文件本身的哈希值。因此,如果您想查找重复项,则必须查找找到两个不同匹配项的实例。

编辑:如果您愿意更改代码,@ senderle上面的答案是更好的方法。