我正在编写一个脚本来查找两个不同文件树中的所有重复文件。该脚本工作正常,除了它太慢而不适用于大量文件(> 1000)。使用cProfile分析我的脚本显示我的代码中的一行几乎负责所有执行时间。
该行是对os.system()的调用:
cmpout = os.system("cmp -s -n 10MiB %s %s" % (callA, callB));
如果我有N个相同的文件,则此调用位于for循环中,调用大约N次。平均执行时间为0.53秒
ncalls tottime percall cumtime percall filename:lineno(function)
563 301.540 0.536 301.540 0.536 {built-in method system}
这当然很快就会增加一千多个文件。我已经尝试通过从子进程模块调用替换它来加速它:
cmpout = call("cmp -s -n 10MiB %s %s" % (callA, callB), shell=True);
但这有一个几乎相同的执行时间。我也试过减少cmp命令本身的字节限制,但这只能节省很少的时间。
无论如何我可以加快速度吗?
我正在使用的全部功能:
def dirintersect(dirA, dirB):
intersectionAB = []
filesA = listfiles(dirA);
filesB = listfiles(dirB);
for (pathB, filenameB) in filesB:
for (pathA, filenameA) in filesA:
if filenameA == filenameB:
callA = shlex.quote(os.path.join(pathA, filenameA));
callB = shlex.quote(os.path.join(pathB, filenameB));
cmpout = os.system("cmp -s -n 10MiB %s %s" % (callA, callB));
#cmpout = call("cmp -s -n 10MiB %s %s" % (callA, callB), shell=True);
if cmpout is 0:
intersectionAB.append((filenameB, pathB, pathA))
return intersectionAB
更新:感谢您的所有反馈!我将尝试解决您的大部分意见并提供更多信息。 @Iarsmans。你用n²我的嵌套for循环标度是完全正确的我已经发现自己可以通过使用字典或设置并执行设置操作来做同样的事情。但即使是这种“糟糕”算法的开销也无关紧要,无法运行os.system。对于每个文件名,实际的if子句大约触发一次(我希望每个文件名只有一个副本)。所以os.system只能运行N次而不是N²次,但即使是这个线性时间也不够快。
@Iarsman和@Alex Reynolds:我没有选择像你建议的哈希解决方案的原因是因为在用例中我设想我将较小的目录树与较大的目录树进行比较并对所有文件进行哈希处理较大的树需要很长时间(因为它可能是整个分区中的所有文件),而我只需要对一小部分文件进行实际比较。@abarnert:我在call命令中使用shell = True的原因仅仅是因为我从os.system开始然后读到最好使用subprocess.call,这也是在它之间进行转换的方法。如果有更好的方法来运行cmp命令,我想知道。我讨论参数的原因是因为当我在命令中传递os.path.join结果时,我在文件名中遇到了空格问题。
感谢您的建议,我将其更改为if cmpout == 0
@Gabe:我不知道如何计时bash命令,但我相信当我运行命令时,它的运行速度比半秒快得多。
我说字节限制并不重要,因为当我将其更改为仅10Kib时,它将我的测试运行的总执行时间改为290秒而不是300秒左右。我保持限制的原因是为了防止它比较真正大的文件(例如1GiB视频文件)。
更新2: 我遵循了@abarnert的建议并将电话改为:
cmpout = call(["cmp", '-s', '-n', '10MiB', callA, callB])
我的测试场景的执行时间现已从300秒降至270秒。还不够,但这是一个开始。
答案 0 :(得分:3)
您使用了错误的算法来执行此操作。比较所有文件对需要n个文件的Θ(n²)时间,而你可以通过散列文件来获得线性时间内两个目录的交集:
from hashlib import sha512
import os
import os.path
def hash_file(fname):
with open(fname) as f:
return sha512(f.read()).hexdigest()
def listdir(d):
return [os.path.join(d, fname) for fname in os.listdir(d)]
def dirintersect(d1, d2):
files1 = {hash_file(fname): fname for fname in listdir(d1)}
return [(files1[hash_file(fname)], fname) for fname in listdir(d2)
if hash_file(fname) in files1]
此函数循环遍历第一个目录,存储由其SHA-512哈希索引的文件名,然后通过在第一个目录中构建的索引中存在具有相同哈希的文件来过滤第二个目录中的文件。一些明显的优化留给读者练习:)
该函数假设目录只包含常规文件或符号链接,并且一次性将文件读入内存(但这并不难修复)。
(SHA-512实际上并不保证文件的相等性,因此可以安装完整的比较作为备份措施,尽管您很难找到具有相同SHA-512的两个文件。)