【问题标题】:Is there a way to efficiently yield every file in a directory containing millions of files?有没有办法有效地产生包含数百万个文件的目录中的每个文件?
【发布时间】:2014-12-26 16:38:30
【问题描述】:

我知道os.listdir,但据我所知,它会将目录中的所有文件名放入内存,然后返回列表。我想要的是一种生成文件名的方法,对其进行处理,然后生成下一个文件名,而无需将它们全部读入内存。

有没有办法做到这一点?我担心使用这种方法更改文件名、添加新文件和删除文件的情况。一些迭代器阻止您在迭代期间修改集合,主要是通过在开始时获取集合状态的快照,并在每个move 操作上比较该状态。如果有一个能够从路径产生文件名的迭代器,如果有文件系统更改(添加、删除、重命名迭代目录中的文件)会修改集合,它是否会引发错误?

可能有几种情况会导致迭代器失败,这完全取决于迭代器如何维护状态。以 S.Lotts 为例:

filea.txt
fileb.txt
filec.txt

迭代器产生filea.txt。在processing 期间,filea.txt 重命名为 filey.txtfileb.txt 重命名为 filez.txt。当迭代器试图获取下一个文件时,如果它使用文件名filea.txt 来查找它的当前位置以便找到下一个文件并且filea.txt 不存在,会发生什么?它可能无法恢复它在集合中的位置。同样,如果迭代器在产生filea.txt 时要获取fileb.txt,它可能会查找fileb.txt 的位置,失败并产生错误。

如果迭代器能够以某种方式维护索引dir.get_file(0),则维护位置状态不会受到影响,但可能会丢失一些文件,因为它们的索引可能会移动到迭代器“后面”的索引。

这当然都是理论上的,因为似乎没有内置 (python) 方法来迭代目录中的文件。但是,下面有一些很好的答案,它们通过使用队列和通知来解决问题。

编辑:

关注的操作系统是 Redhat。我的用例是这样的:

进程 A 不断将文件写入存储位置。 进程 B(我正在编写的那个)将遍历这些文件,根据文件名进行一些处理,并将文件移动到另一个位置。

编辑:

有效定义:

形容词 1. 有充分根据或正当的,相关的。

(抱歉 S.Lott,我无法抗拒)。

我已经编辑了上面有问题的段落。

【问题讨论】:

  • 我认为没有多平台原生 pyhton 方法可以做到这一点 - 你在哪个操作系统上?
  • 将一百万个文件名读入内存真的有问题吗?如今,内存使用实际上是一个问题的情况很少......
  • 一百万个 100 个字符的字符串小于 100 MB 的 RAM ...
  • @Josh Smeaton:在这种情况下,像“有效”这样的广义术语是没有意义的。这个定义没有用,因为这个词太宽泛以至于没有意义。显然,在定义中使用宽泛、模糊、无用的术语是很有趣的。
  • @S.Lott,也许是一次失败的幽默尝试。我想通过像我一样编辑问题,你会意识到我同意你的观察,并试图用潜在的解决方案列举理论化的问题。也许我应该将最初的问题表述为“这些理论化问题中的任何一个在实际实现中是否存在实际问题”。

标签: python list file yield


【解决方案1】:

tl;dr : 从 Python 3.5(目前处于测试阶段)开始,只需使用 os.scandir 更新>

正如我之前所写的,由于“iglob”只是一个真正的迭代器的外观,因此您必须调用低级别的系统函数才能一次获得一个。幸运的是,从 Python 调用低级函数是可行的。 Windows 和 Posix/Linux 系统的底层函数是不同的。

  • 如果您使用的是 Windows,则应检查 win32api 是否有任何调用来读取“来自目录的下一个条目”或如何继续。
  • 如果您使用的是 Posix/Linux,则可以继续直接通过 ctypes 调用 libc 函数,并一次获取一个文件目录条目(包括命名信息)。

关于 C 函数的文档在这里: http://www.gnu.org/s/libc/manual/html_node/Opening-a-Directory.html#Opening-a-Directory

http://www.gnu.org/s/libc/manual/html_node/Reading_002fClosing-Directory.html#Reading_002fClosing-Directory

我提供了一个 Python 代码的 sn-p 代码,它演示了如何在我的系统上调用低级 C 函数,但是这个代码 sn-p 可能无法在您的系统上运行[footnote-1 ]。我建议在使用 sn-p 之前打开您的 /usr/include/dirent.h 头文件并验证 Python sn-p 是否正确(您的 Python Structure 必须与 C struct 匹配)。

这里是使用ctypeslibc 的sn-p,它允许您获取每个文件名并对其执行操作。请注意,当您对结构上定义的 char 数组执行 str(...) 时,ctypes 会自动为您提供 Python 字符串。 (我使用的是print语句,隐式调用Python的str

#!/usr/bin/env python2
from ctypes import *

libc = cdll.LoadLibrary( "libc.so.6")
dir_ = c_voidp( libc.opendir("/home/jsbueno"))

class Dirent(Structure):
    _fields_ = [("d_ino",  c_voidp),
                ("off_t", c_int64),
                ("d_reclen", c_ushort),
                ("d_type", c_ubyte),
                ("d_name", c_char * 2048)
            ]

while True:
    p  = libc.readdir64(dir_)
    if not p:
        break
    entry = Dirent.from_address( p)
    print entry.d_name

更新:Python 3.5 现在处于测试阶段 - 在 Python 3.5 中,新的 os.scandir 函数调用可作为 PEP 471 的实现(“更好更快的目录迭代器”),它完全符合这里的要求,除了许多其他优化之外,在 Windows 下的大型目录列表中,速度可以比 os.listdir 提高多达 9 倍(在 Posix 系统中提高 2-3 倍)。

[footnote-1] dirent64 C struct 在每个系统的 C 编译时确定。

【讨论】:

  • 我要试试os.listdir 方法。如果它产生不可接受的内存使用,我肯定会试一试。很好的答案。
  • 要遍历新写入的文件,@unutbu 的答案中报告的通知方法会更合适。
【解决方案2】:

@jsbueno 的帖子非常有用,但在慢速磁盘上仍然有点慢,因为 libc readdir() 一次只能准备 32K 的磁盘条目。我不是直接在 python 中进行系统调用的专家,但我在博客文章中概述了如何用 C 编写代码来列出包含数百万个文件的目录:http://www.olark.com/spw/2011/08/you-can-list-a-directory-with-8-million-files-but-not-with-ls/

理想的情况是直接在 python (http://www.kernel.org/doc/man-pages/online/pages/man2/getdents.2.html) 中调用 getdents(),以便在从磁盘加载目录条目时指定读取缓冲区大小。

而不是调用 readdir() ,据我所知,它在编译时定义了缓冲区大小。

【讨论】:

    【解决方案3】:

    由于您使用的是 Linux,您可能需要查看 pyinotify。 它将允许您编写一个 Python 脚本来监视目录中的文件系统更改——例如文件的创建、修改或删除。

    每次发生此类文件系统事件时,您都可以安排 Python 脚本调用函数。这大致就像生成每个文件名一次,同时能够对修改和删除做出反应。

    听起来您的目录中已经有一百万个文件。在这种情况下,如果您要将所有这些文件移动到一个新的 pyinotify-monitored 目录,那么由创建新文件生成的文件系统事件将根据需要生成文件名。

    【讨论】:

    • 好一个。我没有尝试像“处理 A”写入文件那样不断更改文件。当然这是去这里的方式。
    • 有趣的是,我已经有一个脚本可以做到这一点 - 我们使用它来在代码更改时自动重新加载我们的 apache wsgi 模块。好主意。
    【解决方案4】:

    我想要的是一种生成文件名的方法,对其进行处理,然后生成下一个文件名,而无需将它们全部读入内存。

    没有方法会显示“更改”的文件名。甚至不清楚您所说的“文件名更改,添加新文件并删除文件”是什么意思?你的用例是什么?

    假设您有三个文件:a.ab.bc.c

    您的神奇“迭代器”以a.a 开头。你处理它。

    神奇的“迭代器”移动到b.b。你正在处理它。

    同时a.a被复制到a1.a1a.a被删除。现在怎么办?你的神奇迭代器用这些做什么?它已经通过a.a。由于a1.a1b.b 之前,它永远不会看到它。 “改文件名,添加新文件,删除文件”应该怎么办?

    神奇的“迭代器”移动到c.c。其他文件应该发生什么?你应该怎么知道删除的?


    进程 A 不断将文件写入存储位置。进程 B(我正在编写的那个)将遍历这些文件,根据文件名进行一些处理,并将文件移动到另一个位置。

    不要使用裸文件系统进行协调。

    使用队列。

    进程 A 写入文件并将添加/更改/删除备忘录排入队列。

    进程B从队列中读取备忘录,然后对备忘录中命名的文件进行后续处理。

    【讨论】:

    • 我向下滚动到“有效”?“有效”是什么意思?并立即知道是您写了答案:P。不过,您提出了很好的观点,我应该更多地充实我的问题,而且我会意识到在我的问题的背景下它并没有多大意义。在某些情况下,我认为在迭代期间更改集合是“非法的”问题。
    • @Joshn Smeaton:“我应该更多地充实我的问题”。你还是可以的。请定义“有效”。或者考虑修改问题以删除未定义的术语。
    【解决方案5】:

    由于文件 IO 的性质,我认为您的要求是不可能的。一旦 python 检索到一个目录的列表,它就无法维护磁盘上实际目录的视图,python 也没有任何办法坚持让操作系统通知它对目录的任何修改。

    python 所能做的就是要求定期列出并比较结果以查看是否有任何变化。

    您能做的最好的事情是在目录中创建一个信号量文件,让其他进程知道您的 python 进程不希望其他进程修改该目录。当然,只有在您明确对其进行编程时,它们才会观察信号量。

    【讨论】:

    • "impossible" 是相对的——你总是可以调用底层的 OS> api——检查我的答案。
    【解决方案6】:

    从 2.5 开始的 glob 模块 Python 有一个 iglob 方法,它返回一个迭代器。 迭代器正是为了不在内存中存储大量值。

    glob.iglob(pathname)
    Return an iterator which yields the same values as glob() without
    actually storing them all simultaneously.
    

    例如:

    import glob
    for eachfile in glob.iglob('*'):
        # act upon eachfile
    

    【讨论】:

    • iglob 似乎是 glob.glob1 的生成器包装器,它返回一个列表。所以整个列表仍然加载到内存中。
    • 沙丘,真的,我注意到了。它正在调用 os.listdir(来自 posixmodule.c),这与调用 ls 非常相似。首先这是一个很好的尝试,如果失败,则应考虑替代方案。谢谢。
    • 无论如何..它认为打开一个针对 bugs.python.org 的功能请求是有效的,请求 iglob 不全局加载名称。
    • 如果 iglob 真的只是 os.listdir 周围的生成器,我想我会使用 listdir 来达到我的目的。不过很好找。
    • 我建议使用 scandir.walk():python.org/dev/peps/pep-0471pypi.python.org/pypi/scandir(适用于 python 2.7)
    猜你喜欢
    • 1970-01-01
    • 2020-09-02
    • 1970-01-01
    • 2012-03-24
    • 1970-01-01
    • 2017-06-02
    • 2016-08-02
    • 2012-09-24
    相关资源
    最近更新 更多