【发布时间】:2014-08-29 22:41:33
【问题描述】:
我正在编写一个脚本来查找两个不同文件树中的所有重复文件。该脚本工作正常,但它太慢而无法处理大量文件(> 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
更新:感谢所有反馈!我将尝试解决您的大多数 cmets 并提供更多信息。 @Iarsmans。你是绝对正确的,我的嵌套 for 循环与 n² 缩放我自己已经发现我可以通过使用字典或集合来做同样的事情并执行集合操作。但即使是这种“坏”算法的开销对于运行 os.system 所花费的时间也是微不足道的。对于每个文件名,实际的 if 子句大约触发一次(也就是说,我希望每个文件名只有一个重复项)。所以 os.system 只运行 N 次而不是 N² 次,但即使对于这个线性时间它也不够快。
@Iarsman 和@Alex Reynolds:我没有像您建议的那样选择散列解决方案的原因是,在我设想的用例中,我将较小的目录树与较大的目录树进行比较,并对目录中的所有文件进行散列较大的树需要很长时间(因为它可能是整个分区中的所有文件),而我只需要对一小部分文件进行实际比较。
@abarnert:我在 call 命令中使用 shell=True 的原因仅仅是因为我从 os.system 开始,然后读到使用 subprocess.call 更好,这也是在两者之间进行转换的方式。如果有更好的方法来运行 cmp 命令,我想知道。我 qoute 参数的原因是,当我刚刚在命令中传递 os.path.join 结果时,文件名中的空格存在问题。
谢谢你的建议,我改成if cmpout == 0
@Gabe:我不知道如何为 bash 命令计时,但我相信当我只运行命令时它的运行速度要比半秒快得多。
我说字节限制并不重要,因为当我将其更改为仅 10Kib 时,它会将我的测试运行的总执行时间更改为 290 秒,而不是大约 300 秒。我保留限制的原因是为了防止它比较非常大的文件(例如 1GiB 视频文件)。
更新 2: 我已遵循@abarnert 的建议并将调用更改为:
cmpout = call(["cmp", '-s', '-n', '10MiB', callA, callB])
我的测试场景的执行时间现在从 300 秒下降到 270 秒。还不够,但这是一个开始。
【问题讨论】:
-
产生一个子进程总是相当缓慢。在 Python 中重新实现
cmp可能会更快。您可以做的另一个优化是检查文件大小;如果它们不同,那么您就知道文件不同。 -
你为什么要使用 shell?您没有将它用于任何事情,而且您显然是在浪费时间在
cmp程序之上启动和退出 shell。当然,它让事情变得更加复杂(你必须quote的论点)。我不知道这是否是你的问题,但为什么要这样做呢?只需call(['cmp', '-s', '-n', '10MiB', os.path.join(pathA, filenameA), os.path.join(pathB, filenameB)])。 -
哈希(例如 SHA1)每个被比较的文件。将每个散列和路径存储在字典中。如果您尝试再次存储哈希(如果密钥已存在),则存储路径和当前路径指向重复文件。
-
cmp无论您是否通过 Python 运行它所花费的时间都相同吗? -
附带说明,
cmpout is 0几乎永远不会正确。您正在检查,不仅call或system返回0,而且Python 恰好只有一次0实例被实习并为返回0的任何内容返回该值。这对于 CPython 来说是正确的,除非您在构建时禁用小数优化,但为什么要依赖它呢?只需执行if cmpout == 0或if not cmpout。
标签: python performance os.system