48

我在解析文件夹中的 1000 个文本文件时遇到问题(每个文件大约 3000 行,大小约为 400KB)。我确实使用 readlines 阅读了它们,

   for filename in os.listdir (input_dir) :
       if filename.endswith(".gz"):
          f = gzip.open(file, 'rb')
       else:
          f = open(file, 'rb')

       file_content = f.readlines()
       f.close()
   len_file = len(file_content)
   while i < len_file:
       line = file_content[i].split(delimiter) 
       ... my logic ...  
       i += 1  

这对于我输入的样本(50,100 个文件)完全适用。当我在整个输入上运行超过 5K 文件时,所花费的时间远不接近线性增量。我计划进行性能分析并进行 Cprofile 分析。当输入达到 7K 文件时,更多文件所花费的时间随着达到更差的速率而呈指数增长。

这是 readlines 的累积时间,第一个 -> 354 个文件(来自输入的样本)和第二个 -> 7473 个文件(整个输入)

 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
 354    0.192    0.001    **0.192**    0.001 {method 'readlines' of 'file' objects}
 7473 1329.380    0.178  **1329.380**    0.178 {method 'readlines' of 'file' objects}

因此,我的代码所花费的时间不会随着输入的增加而线性缩放。我在 上阅读了一些文档注释readlines(),人们声称这会将整个文件内容读入内存,因此与orreadlines()相比通常会消耗更多内存。readline()read()

我同意这一点,但是垃圾收集器是否应该在循环结束时自动从内存中清除加载的内容,因此在任何时候我的内存都应该只有我当前处理的文件的内容,对吗?但是,这里有一些问题。有人可以对这个问题提供一些见解。

这是python垃圾收集器的固有行为readlines()还是我对python垃圾收集器的错误解释。很高兴知道。

此外,建议一些替代方法在内存和时间效率方面做同样的事情。TIA。

4

2 回答 2

97

简短的版本是:有效的使用方法readlines()是不使用它。曾经。


我在 上阅读了一些文档注释readlines(),人们声称这readlines()会将整个文件内容读入内存,因此与 readline() 或 read() 相比通常会消耗更多内存。

for 的文档明确保证它将整个文件读入内存,并将其解析为行,并从这些行中构建完整的ings。readlines() liststr

但是read()同样的文档保证它将整个文件读入内存,并构建一个string,所以这没有帮助。


除了使用更多内存之外,这还意味着在读取整个内容之前您无法进行任何工作。如果您以最幼稚的方式交替读取和处理,您将至少从一些流水线中受益(感谢 OS 磁盘缓存、DMA、CPU 流水线等),因此您将在处理一个批次的同时处理下一批正在阅读。但是如果你强制计算机读取整个文件,然后解析整个文件,然后运行你的代码,你只会得到整个文件的一个重叠工作区域,而不是每次读取的一个重叠工作区域。


您可以通过三种方式解决此问题:

  1. readlines(sizehint)在、read(size)或周围写一个循环readline()
  2. 只需将文件用作惰性迭代器而不调用任何这些。
  3. mmap该文件,它允许您将其视为一个巨大的字符串,而无需先读取它。

例如,这必须foo一次读取所有内容:

with open('foo') as f:
    lines = f.readlines()
    for line in lines:
        pass

但这一次只能读取大约 8K:

with open('foo') as f:
    while True:
        lines = f.readlines(8192)
        if not lines:
            break
        for line in lines:
            pass

这一次只能读取一行——尽管 Python 被允许(并且将)选择一个合适的缓冲区大小来加快速度。

with open('foo') as f:
    while True:
        line = f.readline()
        if not line:
            break
        pass

这将做与以前完全相同的事情:

with open('foo') as f:
    for line in f:
        pass

同时:

但是垃圾收集器是否应该在循环结束时自动从内存中清除加载的内容,因此在任何时候我的内存都应该只有我当前处理的文件的内容,对吗?

Python 不对垃圾收集做出任何此类保证。

CPython 实现恰好使用 GC 引用计数,这意味着在您的代码中,一旦file_content反弹或消失,巨大的字符串列表以及其中的所有字符串都将被释放到 freelist,这意味着相同内存可以再次用于您的下一次传递。

然而,所有这些分配、复制和解除分配都不是免费的——不做比做要快得多。

最重要的是,让您的字符串分散在一大片内存中,而不是一遍又一遍地重用同一小块内存会损害您的缓存行为。

另外,虽然内存使用量可能是恒定的(或者更确切地说,与最大文件的大小成线性关系,而不是文件大小的总和),malloc第一次扩展它的 s 将是最慢的一个您所做的事情(这也使得进行性能比较变得更加困难)。


综上所述,这就是我编写程序的方式:

for filename in os.listdir(input_dir):
    with open(filename, 'rb') as f:
        if filename.endswith(".gz"):
            f = gzip.open(fileobj=f)
        words = (line.split(delimiter) for line in f)
        ... my logic ...  

或者可能:

for filename in os.listdir(input_dir):
    if filename.endswith(".gz"):
        f = gzip.open(filename, 'rb')
    else:
        f = open(filename, 'rb')
    with contextlib.closing(f):
        words = (line.split(delimiter) for line in f)
        ... my logic ...
于 2013-06-22T00:55:10.857 回答
18

逐行读取,而不是整个文件:

for line in open(file_name, 'rb'):
    # process line here

更好地with用于自动关闭文件:

with open(file_name, 'rb') as f:
    for line in f:
        # process line here

上面将使用迭代器读取文件对象,一次一行。

于 2013-06-22T00:49:34.673 回答