我需要处理的数据比内存大几百倍。 我想读入一个大块,处理它,保存结果,释放内存并重复执行。 有没有办法在Python中使这个过程更加高效?
一般的关键是你想要迭代地处理文件。
如果你只是处理文本文件,这很简单: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]
这种代码在网络编程中非常常见(因为套接字
不能只是“读取全部”,所以您总是需要读取到缓冲区并将其分成消息),因此您可能会在使用类似于您的文件格式的协议的网络代码中找到一些有用的示例。
或者,您可以使用mmap
。
如果您的虚拟内存大小大于文件大小,则这很简单:
with mmap.mmap(f.fileno(), access=mmap.ACCESS_READ) as m:
process(m)
现在m
像一个巨大的bytes
对象,就好像你调用了read()
将整个文件读入内存一样——但是操作系统会根据需要自动分页内存中的位。
如果您尝试读取太大以至于无法适应虚拟内存大小的文件(例如使用32位Python的4GB文件,或者使用64位Python的20EB文件——只有当您读取像Linux上另一个进程的VM文件这样的稀疏或虚拟文件时才可能发生这种情况),您必须实现窗口化——每次映射一部分文件。例如:
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窗口”的内容,如果遇到困难,请编写一个新问题。/proc
文件系统中读取,对于大多数其他的*nix系统,可以使用ctypes
到sysctlbyname
,对于Windows,则可以使用win2api
到GlobalMemoryStatusEx
等方式。 - abarnert
mmap
和read
在底层实现上的区别等有用的讨论。 - abarnert