我需要处理一些比 RAM 大几百倍的数据。我想读一大块,处理它,保存结果,释放内存并重复。有没有办法在 python 中提高效率?
1 回答
一般的关键是您要迭代地处理文件。
如果您只是处理一个文本文件,这很简单:for line in f:
一次只读取一行。(实际上它缓冲了一些东西,但缓冲区足够小,你不必担心它。)
如果您正在处理一些其他特定文件类型,例如 numpy 二进制文件、CSV 文件、XML 文档等,通常有类似的专用解决方案,但除非您告诉我们是什么,否则没有人可以向您描述它们你有什么样的数据。
但是如果你有一个通用的二进制文件呢?
首先,该read
方法需要一个可选的最大字节数来读取。所以,而不是这个:
data = f.read()
process(data)
你可以这样做:
while True:
data = f.read(8192)
if not data:
break
process(data)
你可能想写一个这样的函数:
def chunks(f):
while True:
data = f.read(8192)
if not data:
break
yield data
然后你可以这样做:
for chunk in chunks(f):
process(chunk)
你也可以用两个参数来做到这一点iter
,但很多人觉得这有点晦涩:
for chunk in iter(partial(f.read, 8192), b''):
process(chunk)
无论哪种方式,此选项都适用于以下所有其他变体(除了单个mmap
,这是微不足道的,没有意义)。
那里的数字 8192 没有什么神奇之处。您通常确实需要 2 的幂,理想情况下是系统页面大小的倍数。除此之外,无论您使用 4KB 还是 4MB,您的性能都不会有太大变化——如果是这样,您将必须测试最适合您的用例的方法。
无论如何,这假设您可以一次只处理每个 8K,而无需保留任何上下文。例如,如果您将数据馈送到渐进式解码器或哈希器或其他东西,那是完美的。
但是,如果您需要一次处理一个“块”,那么您的块最终可能会跨越 8K 边界。你怎么处理?
这取决于您的块在文件中的分隔方式,但基本思想非常简单。例如,假设您使用 NUL 字节作为分隔符(不太可能,但作为玩具示例很容易展示)。
data = b''
while True:
buf = f.read(8192)
if not buf:
process(data)
break
data += buf
chunks = data.split(b'\0')
for chunk in chunks[:-1]:
process(chunk)
data = chunks[-1]
这种代码在网络中很常见(因为sockets
不能只是“读取所有”,所以你总是必须读入缓冲区并将消息分块),所以你可能会在使用类似协议的网络代码中找到一些有用的示例到您的文件格式。
或者,您可以使用mmap
.
如果您的虚拟内存大小大于文件,这很简单:
with mmap.mmap(f.fileno(), access=mmap.ACCESS_READ) as m:
process(m)
现在m
就像一个巨大的bytes
对象,就像您调用read()
将整个内容读入内存一样——但操作系统会根据需要自动将位分页进出内存。
如果您尝试读取一个太大而无法适应您的虚拟内存大小的文件(例如,使用 32 位 Python 的 4GB 文件,或使用 64 位 Python 的 20EB 文件——如果您在 2013 年才可能发生这种情况) '正在读取一个稀疏或虚拟文件,比如 Linux 上另一个进程的 VM 文件),你必须实现窗口化——一次在一个文件中进行 mmap。例如:
windowsize = 8*1024*1024
size = os.fstat(f.fileno()).st_size
for start in range(0, size, window size):
with mmap.mmap(f.fileno(), access=mmap.ACCESS_READ,
length=windowsize, offset=start) as m:
process(m)
当然,如果您需要分隔事物,映射窗口与读取块有相同的问题,您可以以相同的方式解决它。
但是,作为一种优化,您可以将窗口向前滑动到包含最后一条完整消息末尾的页面,而不是一次 8MB,而不是缓冲,这样就可以避免任何复制。这有点复杂,所以如果你想这样做,请搜索“滑动 mmap 窗口”之类的内容,如果遇到困难,请编写一个新问题。