6

我正在尝试通过一个大文本文件(~232GB)搜索一些关键字。我想利用缓冲来解决速度问题,还想记录包含这些关键字的行的开始位置。

我在这里看到很多帖子讨论类似的问题。然而,那些带有缓冲的解决方案(使用文件作为迭代器)不能给出正确的文件位置,而那些给出正确文件位置的解决方案通常只是简单地使用f.readline(),它不使用缓冲。

我看到的唯一可以同时做到的答案是这里

# Read in the file once and build a list of line offsets
line_offset = []
offset = 0
for line in file:
    line_offset.append(offset)
    offset += len(line)
file.seek(0)

# Now, to skip to line n (with the first line being line 0), just do
file.seek(line_offset[n])

但是,我不确定该offset += len(line)操作是否会花费不必要的时间。有没有更直接的方法来做到这一点?

更新:

我已经做了一些计时,但它似乎.readline()比使用文件对象作为迭代器要慢得多,在python 2.7.3. 我使用了以下代码

#!/usr/bin/python

from timeit import timeit

MAX_LINES = 10000000

# use file object as iterator
def read_iter(): 
    with open('tweets.txt','r') as f:
        lino = 0
        for line in f:
            lino+=1
            if lino == MAX_LINES:
                break

# use .readline()
def read_readline(): 
    with open('tweets.txt', 'r') as f:
        lino = 0
        for line in iter(f.readline,''):
            lino+=1
            if lino == MAX_LINES:
                break

# use offset+=len(line) to simulate f.tell() under binary mode
def read_iter_tell(): 
    offset = 0
    with open('tweets.txt','rb') as f:
        lino = 0
        for line in f:
            lino+=1
            offset+=len(line)
            if lino == MAX_LINES:
                break

# use f.tell() with .readline()
def read_readline_tell():
    with open('tweets.txt', 'rb') as f:
        lino = 0
        for line in iter(f.readline,''):
            lino+=1
            offset = f.tell()
            if lino == MAX_LINES:
                break

print ("iter: %f"%timeit("read_iter()",number=1,setup="from __main__ import read_iter"))
print ("readline: %f"%timeit("read_readline()",number=1,setup="from __main__ import read_readline"))
print ("iter_tell: %f"%timeit("read_iter_tell()",number=1,setup="from __main__ import read_iter_tell"))
print ("readline_tell: %f"%timeit("read_readline_tell()",number=1,setup="from __main__ import read_readline_tell"))

结果是这样的:

iter: 5.079951
readline: 37.333189
iter_tell: 5.775822
readline_tell: 38.629598
4

1 回答 1

9

使用有什么问题.readline()

您发现的示例对于以文本模式打开的文件不正确。它在 Linux 系统上应该可以正常工作,但在 Windows 上则不行。在 Windows 上,返回到文本模式文件中先前位置的唯一方法是寻找以下位置之一:

  1. 0(文件开头)。

  2. 文件结束。

  3. 以前返回的职位f.tell()

您不能以任何可移植的方式计算文本模式文件的位置。

所以使用.readline(), 和/或.read(), 和.tell(). 问题解决了 ;-)

关于缓冲:是否使用缓冲与文件的访问方式无关;它完全与文件的打开方式有关。缓冲是一个实现细节。特别是,f.readline()肯定是在幕后缓冲的(除非您在文件open()调用中明确禁用缓冲),但以您看不到的方式。使用文件对象作为迭代器发现的问题与文件迭代器实现添加的额外缓冲层有关(file.next()文档称之为“隐藏的预读缓冲区”)。

要回答您的其他问题,费用如下:

offset += len(line)

是微不足道的 - 但是,如前所述,“解决方案”存在实际问题。

短期课程:不要过早地变得棘手。做最简单的有效的事情(比如.readline()+ .tell()),只有在证明是不够的时候才开始担心。

更多细节

实际上有几层缓冲正在进行。在硬件中,您的磁盘驱动器内部有内存缓冲区。除此之外,您的操作系统也维护内存缓冲区,并且通常在您以统一模式访问文件时尝试“智能”,要求磁盘驱动器在您正在读取的方向上“预读”磁盘块,超出您已经要求的块。

CPython 的 I/O 构建在平台 C 的 I/O 库之上。C 库有自己的内存缓冲区。为了让 Python f.tell()“正常工作”,CPython 必须以 C 规定的方式使用 C 库。

现在,关于“一条线”(嗯,在任何主要操作系统上都没有),这一切都没有什么特别之处。“一行”是一个软件概念,通常仅表示“直到并包括下一个\n字节(Linux)、\r字节(某些 Mac 版本)或\r\n字节对(Windows)。硬件、操作系统和 C 缓冲区通常不会对“行”一无所知——它们只处理字节流。

在幕后,Python.readline()本质上是一次“读取”一个字节,直到它看到平台的行尾字节序列(\n、、\r\r\n)。我将“读取”放在引号中,因为通常不涉及磁盘访问 - 它通常只是各个级别的软件从其内存缓冲区复制字节。当涉及磁盘访问时它会慢数千倍。

通过这样做“一次一个字节”,C 级库为f.tell(). 但要付出代价:获得的每个字节可能会有多层函数调用。

Python 的文件迭代器一次将字节“读取”到它自己的内存缓冲区中。“多少”无关紧要;-) 重要的是它要求 C 库一次复制多个字节,然后 CPython 在其自己的内存缓冲区中搜索行尾序列。这减少了所需的函数调用次数。但代价不同:C 库关于我们在文件中的位置的想法反映了读入文件迭代器内存缓冲区的字节数,这与用户的 Python 程序检索到的字节数无关从那个缓冲区。

所以,是的,确实for line in file:是逐行浏览整个文本文件的最快方法。

有关系吗?唯一确定的方法是根据真实数据进行计时。如果要读取 200+ GB 的文件,您将花费数千倍的时间进行物理磁盘读取,而不是软件的各个层搜索行尾字节序列所花费的时间。

如果事实证明它确实很重要,并且您的数据和操作系统使得您可以以二进制模式打开文件并仍然获得正确的结果,那么您找到的代码片段将提供两全其美(最快的行迭代和正确的字节位置供以后使用.seek())。

于 2013-10-20T00:55:21.750 回答