一般的关键是你要迭代地处理文件。
如果您只是处理一个文本文件,这很简单: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 窗口”之类的内容,如果遇到困难,请编写一个新问题。