为什么readlines()读取的内容远大于sizehint?

时间:2014-09-10 01:29:51

标签: python parsing readlines

背景

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

问题

为什么不注意尺寸提示?

示例代码

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

sizehint

结果

我使用大约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,所有的开始/停止位置都按照应有的方式排列,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) 报告的实际文件位置似乎是正确的,所以我是相当肯定我的分块算法是好的。但是,实际停止位置显示fi.tell()读取的内容远远超过大小提示。此外,输出文件#1 - #8是8.0KB,远大于提示大小。

即使我尝试仅在线端打破块也是错误的,readlines()仍然不应该读取超过2KB +一行。文件#9 - #12变得越来越小,这是有道理的,因为块起始点越来越接近文件的末尾,并且readlines()不会读取超过文件的末尾。

备注

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

2 个答案:

答案 0 :(得分:2)

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的缓冲区,所以你应该让你的块大于那个。

答案 1 :(得分:0)

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

我觉得可能有更好的方法 chunk 你的文件,这会绕过你当前的问题。只是一个想法,但由于文件可以迭代,这样的工作会起作用吗?

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。工人可以从队列中拉出块。