有没有高效遍历包含数百万文件的目录的方法?
我知道有个叫 os.listdir
的东西,但我了解到它是把一个文件夹里的所有文件名都加载到内存里,然后再返回这个列表。我想要的是一种方法,可以一个一个地获取文件名,处理完一个再获取下一个,而不是一次性把所有文件名都读到内存里。
有没有办法做到这一点呢?我担心在这种方法下,文件名会变化,比如新文件被添加、旧文件被删除等。有些迭代器在你遍历的时候不允许你修改集合,基本上是先拍个快照,记录当时的状态,然后在每次 move
操作时对比这个状态。如果有一个迭代器能从某个路径中获取文件名,当文件系统发生变化(比如添加、删除、重命名文件)时,它会报错吗?
可能会有一些情况导致迭代器失败,这完全取决于迭代器是如何维护状态的。用 S.Lott 的例子来说明:
filea.txt
fileb.txt
filec.txt
迭代器返回 filea.txt
。在 processing
的过程中,filea.txt
被重命名为 filey.txt
,而 fileb.txt
被重命名为 filez.txt
。当迭代器尝试获取下一个文件时,如果它用 filea.txt
来查找当前位置,但 filea.txt
已经不在了,那会发生什么呢?它可能无法找回在集合中的位置。同样的,如果迭代器在返回 filea.txt
时去获取 fileb.txt
,可能会查找失败,导致报错。
如果迭代器能够以某种方式维护一个索引 dir.get_file(0)
,那么位置状态就不会受到影响,但有些文件可能会被遗漏,因为它们的索引可能会被移动到迭代器“后面”的位置。
当然,这些都是理论上的情况,因为目前似乎没有内置的(Python)方法可以遍历文件夹里的文件。不过,下面有一些很好的答案,通过使用队列和通知来解决这个问题。
编辑:
我关心的操作系统是 Redhat。我的使用场景是这样的:
进程 A 不断地往一个存储位置写文件。
进程 B(就是我正在写的这个)会遍历这些文件,根据文件名进行一些处理,然后把文件移动到另一个位置。
编辑:
有效的定义:
形容词
1. 有充分的理由或正当的,相关的。
(抱歉,S.Lott,我忍不住了)。
我已经编辑了上面提到的段落。
6 个回答
既然你在用Linux系统,可以看看pyinotify这个工具。它可以让你写一个Python脚本,监控一个文件夹里的变化,比如文件的创建、修改或者删除。
每当发生这样的文件变化时,你可以让这个Python脚本调用一个函数。简单来说,就是每当有文件名出现时,你都能处理一下,同时还能对文件的修改和删除做出反应。
听起来你已经有很多文件放在一个文件夹里了。如果是这样的话,你可以把这些文件移动到一个新的文件夹,然后用pyinotify来监控这个新文件夹。这样,当新文件被创建时,系统就会自动生成文件名,正好满足你的需求。
从Python 2.5开始,glob模块里有一个叫做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
简而言之 <更新>: 从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
我提供了一段Python代码示例,演示如何在我的系统上调用底层C函数,但这段代码在你的系统上可能无法运行[脚注-1]。我建议你打开/usr/include/dirent.h
头文件,确认这段Python代码是正确的(你的Python结构
必须与C的struct
匹配)后再使用这段代码。
下面是我用ctypes
和libc
写的代码片段,可以让你获取每个文件名,并对其进行操作。请注意,当你对结构中定义的字符数组使用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的大目录列表中提供高达9倍的速度提升(在Posix系统中提升2-3倍)。
[脚注-1] dirent64
C struct
是在每个系统的C编译时确定的。