【问题标题】:Why is readlines() reading much more than the sizehint?为什么 readlines() 比 sizehint 读得更多?
【发布时间】:2014-11-03 13:05:42
【问题描述】:

背景

我在 Python 2.7.6 中解析非常大的文本文件 (30GB+)。为了加快这个过程,我将文件分成块并使用多处理库将它们分流到子进程。为此,我在我的主进程中迭代文件,记录我想要拆分输入文件的字节位置并将这些字节位置传递给子进程,然后打开输入文件并使用file.readlines(chunk_size)读取它们的块.但是,我发现读入的块似乎比sizehint 参数大得多(4x)。

问题

为什么不注意 sizehint?

示例代码

以下代码演示了我的问题:

import sys

# set test chunk size to 2KB
chunk_size = 1024 * 2

count = 0
chunk_start = 0
chunk_list = []

fi = open('test.txt', 'r')
while True:
    # increment chunk counter
    count += 1

    # calculate new chunk end, advance file pointer
    chunk_end = chunk_start + chunk_size
    fi.seek(chunk_end)

    # advance file pointer to end of current line so chunks don't have broken 
    # lines
    fi.readline() 
    chunk_end = fi.tell()

    # record chunk start and stop positions, chunk number
    chunk_list.append((chunk_start, chunk_end, count))

    # advance start to current end
    chunk_start = chunk_end

    # read a line to confirm we're not past the end of the file
    line = fi.readline()
    if not line:
        break

    # reset file pointer from last line read
    fi.seek(chunk_end, 0)

fi.close()

# This code represents the action taken by subprocesses, but each subprocess
# receives one chunk instead of iterating the list of chunks itself.
with open('test.txt', 'r', 0) as fi:
    # iterate over chunks
    for chunk in chunk_list:
        chunk_start, chunk_end, chunk_num = chunk

        # advance file pointer to chunk start
        fi.seek(chunk_start, 0)

        # print some notes and read in the chunk
        sys.stdout.write("Chunk #{0}: Size: {1} Start {2} Real Start: {3} Stop {4} "
              .format(chunk_num, chunk_end-chunk_start, chunk_start, fi.tell(), chunk_end))
        chunk = fi.readlines(chunk_end - chunk_start)
        print("Real Stop: {0}".format(fi.tell()))

        # write the chunk out to a file for examination
        with open('test_chunk{0}'.format(chunk_num), 'w') as fo:
            fo.writelines(chunk)

结果

我使用大约 23.3KB 的输入文件 (test.txt) 运行此代码,它产生了以下输出:

块 #1:大小:2052 开始 0 实际开始:0 停止 2052 实际停止:8193
块 #2:大小:2051 开始 2052 实际开始:2052 停止 4103 实际停止:10248
块 #3:大小:2050 开始 4103 实际开始:4103 停止 6153 实际停止:12298
块 #4:大小:2050 开始 6153 实际开始:6153 停止 8203 实际停止:14348
块 #5:大小:2050 开始 8203 实际开始:8203 停止 10253 实际停止:16398
块 #6:大小:2050 开始 10253 实际开始:10253 停止 12303 实际停止:18448
块 #7:大小:2050 开始 12303 实际开始:12303 停止 14353 实际停止:20498
块 #8:大小:2050 开始 14353 实际开始:14353 停止 16403 实际停止:22548
块 #9:大小:2050 开始 16403 实际开始:16403 停止 18453 实际停止:23893
块 #10:大小:2050 开始 18453 实际开始:18453 停止 20503 实际停止:23893
块 #11:大小:2050 开始 20503 实际开始:20503 停止 22553 实际停止:23893
块 #12:大小:2048 开始 22553 实际开始:22553 停止 24601 实际停止:23893

报告的每个块大小约为 2KB,所有开始/停止位置都按应有的方式排列,fi.tell() 报告的真实文件位置似乎是正确的,所以我相当确定我的分块算法不错。然而,真正的停止位置显示readlines() 的读数远远超过尺寸提示。此外,输出文件 #1 - #8 为 8.0KB,远大于大小提示。

即使我尝试只在行尾拆分块是错误的,readlines() 仍然不应该读取超过 2KB + 一行的内容。文件 #9 - #12 变得越来越小,这是有道理的,因为块起始点越来越接近文件的末尾,而 readlines() 不会读到文件末尾。

注意事项

  1. 我的测试输入文件只是在每一行打印了“\n”,1-5000。
  2. 我再次尝试了不同的块和输入文件大小,结果相似。
  3. readlines documentation 表示读取大小可能会四舍五入到内部缓冲区的大小,因此我尝试在不使用缓冲的情况下打开文件(如图所示),但没有任何区别。
  4. 我使用这个算法来分割文件,因为我需要能够支持*.bz2和*.gz压缩文件,而*.gz文件没有办法让我在不解压文件的情况下识别未压缩文件的大小. *.bz2 文件也没有,但我可以从这些文件末尾寻找 0 个字节并使用 fi.tell() 来获取文件大小。见my related question
  5. 在添加支持压缩文件的要求之前,以前版本的脚本使用 os.path.getsize() 作为分块循环的停止条件,并且 readlines 似乎可以很好地使用该方法。

【问题讨论】:

  • 它确实说 sizehint 而不是 size...
  • @monkut 是的,但是当我建议 2KB 时,它没有理由应该读取 8KB,假设它不必读取另一个 6KB 来读取完整的行。它应该读取建议的字节数,但是需要很多字节才能到达下一个换行符。此外,正如我在笔记中提到的,在我更改分块算法之前,readlines 过去一直遵循 sizehint。
  • 实际停止位最多比您的预期停止位多几个 kB。对于现代 CPU 和 GiB 内存来说,这是一个非常小的数量,这可能是由于在大多数操作系统上以页面大小的块或更大的块分配内存的效率大大提高。你的程序到底有多大问题?
  • 您的增量似乎是.readline(),并且行由换行符分隔。虽然您假设换行符出现在 2KB 左右,但换行符的位置是任意的。
  • @DanLenski 这是一个问题,因为这意味着我要多次读取相同的数据。我正在尝试将文件拆分为不重叠的块以供子进程处理,否则当我实际处理文件解析的结果时,我最终会得到重复的数据。此外,这是我看到的问题的一个小示例文件。我的真实输入文件可能有几十 GB。

标签: python parsing readlines


【解决方案1】:

readlines 文档中提到的缓冲区与 open 调用的第三个参数控制的缓冲区无关。缓冲区是this buffer in file_readlines:

static PyObject *
file_readlines(PyFileObject *f, PyObject *args)
{
    long sizehint = 0;
    PyObject *list = NULL;
    PyObject *line;
    char small_buffer[SMALLCHUNK];

SMALLCHUNK 在前面定义:

#if BUFSIZ < 8192
#define SMALLCHUNK 8192
#else
#define SMALLCHUNK BUFSIZ
#endif

我不知道BUFSIZ 来自哪里,但看起来你得到了#define SMALLCHUNK 8192 案例。在任何情况下,readlines 永远不会使用小于 8 KiB 的缓冲区,所以你应该让你的块更大。

【讨论】:

  • 这看起来很有希望,但我在一个约 150KB 的测试文件上运行了 16KB 块大小的测试,并且这些块被四舍五入为 24KB。有什么建议 readlines() 将读取到最近的 multiple 缓冲区大小?
  • @ScottLawson:如果您正在考虑建议它的代码,那么是的。 (不过,这比这要复杂一些。)如果您正在考虑文档,这就是我阅读“可能在四舍五入到内部缓冲区大小之后”行的方式。无论如何,2 KiB 很小。您看到的行为就像要求复印书页上的第一段并获得整页的复印件,或者要求 2 页和一个段落并获得 3 页。没有那么不合理。
【解决方案2】:

这并不能回答您的问题,但也许会有所帮助...

我感觉可能有更好的方法来分块您的文件,这样可以绕过您当前的问题。只是一个想法,但既然文件可以被迭代,这样的事情会起作用吗?

import bzip2
import gzip
from multiprocessing import Pool, cpu_count


def chunker(filepath):
    """define and yield chunks"""
    if filepath.endswith(".bz"):
        read_open = bzip2.open
    elif filepath.endswith(".gz"):
        read_open = gzip.open

    with read_open(filepath) as in_f:        
        delim = "something"
        chunk = []
        for line in in_f:
            if delim not in line:
                chunk.append(line)
            else:
                current, next_ = line.split(delim)
                chunk.append(current)
                yield chunk
                chunk = [next_]
        if chunk:
            yield chunk

def process_chunk(chunk):
    # do magic
    return 

if __name__ == '__main__':
    filepath = ""
    chunk_iter = chunker(filepath)

    pool = Pool(processes=cpu_count() - 1)
    for result in pool.imap(process_chunk, chunk_iter , chunksize=1)
        print result

或者如果您已经使用 1-pass 来读取和生成块列表,为什么不在读取时将单独的块写为单独的文件(如果您有磁盘空间)。然后你可以给一个工作池一个要处理的文件路径列表。

或者,如果您的工作人员足够快地处理块并且您有内存,您可以在阅读时将整个块传递给Queue。并且工人可以从队列中拉出块。

【讨论】:

  • 最大的块大小是多少?
  • 不幸的是,您的代码 sn-p 对我不起作用。除了需要确保块不会断行之外,我还需要查看行的实际内容,并且只在某些标记之间中断块。我只是把它从这个例子中去掉,以使其更短。不过,我会考虑你的最后一个建议。也许一旦它们存在就将它们保留在内存中会是一种更好的方法......
  • 这取决于我。我打算在完成某些工作后尝试使用块大小,看看是什么让我的运行时间最短。幸运的是,我有一些相当大的计算资源可用,所以工作内存占用不是什么大问题,所以一次在内存中保留许多大块应该不会造成问题。
  • 如果你不想和队列混在一起,你可以给 pool.imap() 一个块迭代器。
猜你喜欢
  • 1970-01-01
  • 2014-11-07
  • 1970-01-01
  • 2015-07-14
  • 1970-01-01
  • 2017-12-26
  • 2011-01-25
  • 2012-12-20
  • 2014-04-21
相关资源
最近更新 更多