17

我在一个文件中有一个 ASCII 表,我想从中读取一组特定的行(例如第 4003 到 4005 行)。问题是这个文件可能非常长(例如,几千到几百万行),我想尽快这样做。

错误的解决方案:读入整个文件,然后转到那些行,

f = open('filename')
lines = f.readlines()[4003:4005]

更好的解决方案enumerate在每一行上,这样它就不会全部在内存中(a la https://stackoverflow.com/a/2081880/230468

f = open('filename')
lines = []
for i, line in enumerate(f):
    if i >= 4003 and i <= 4005: lines.append(line)
    if i > 4005: break                                    # @Wooble

最佳解决方案?

但这仍然需要遍历每一行。是否有更好的(就速度/效率而言)访问特定线路的方法?即使我只会访问一次文件(通常),我是否应该使用linecache ?

使用二进制文件代替,在这种情况下可能更容易跳过,是一种选择——但我宁愿避免它。

4

3 回答 3

19

我可能只会使用itertools.islice. 在像文件句柄这样的可迭代对象上使用 islice 意味着永远不会将整个文件读入内存,并且会尽快丢弃前 4002 行。你甚至可以很便宜地将你需要的两行放入一个列表中(假设这些行本身不是很长)。然后你可以退出with块,关闭文件句柄。

from itertools import islice
with open('afile') as f:
    lines = list(islice(f, 4003, 4005))
do_something_with(lines)

更新

但是对于多次访问来说,圣牛的 linecache 速度更快。我创建了一个百万行的文件来比较 islice 和 linecache,而 linecache 把它吹走了。

>>> timeit("x=islice(open('afile'), 4003, 4005); print next(x) + next(x)", 'from itertools import islice', number=1)
4003
4004

0.00028586387634277344
>>> timeit("print getline('afile', 4003) + getline('afile', 4004)", 'from linecache import getline', number=1)
4002
4003

2.193450927734375e-05

>>> timeit("getline('afile', 4003) + getline('afile', 4004)", 'from linecache import getline', number=10**5)
0.14125394821166992
>>> timeit("''.join(islice(open('afile'), 4003, 4005))", 'from itertools import islice', number=10**5)
14.732316970825195

不断地重新导入和重新读取文件:

这不是一个实际测试,但即使在每一步重新导入 linecache 也只比 islice 慢一秒。

>>> timeit("from linecache import getline; getline('afile', 4003) + getline('afile', 4004)", number=10**5)
15.613967180252075

结论

是的,除了不断地重新创建 linecache 之外,linecache 比 islice 快,但是谁这样做呢?对于可能的情况(一次只读取几行,一次读取多行),linecache 更快并且呈现简洁的语法,但islice语法也非常干净和快速,并且不会将整个文件读入内存. 在 RAM 紧张的环境中,该islice解决方案可能是正确的选择。对于非常高的速度要求,linecache 可能是更好的选择。但实际上,在大多数环境中,这两个时间都足够小,几乎无关紧要。

于 2013-10-04T20:27:49.373 回答
8

这里的主要问题是,换行符与任何其他字符没有任何不同。所以操作系统没有办法跳到那一行

也就是说,有几种选择,但对于每一种选择,您都必须以一种或另一种方式做出牺牲。

您已经说明了第一个:使用二进制文件。如果您有固定的行长,那么您可以seek提前line * bytes_per_line字节并直接跳转到该行。

下一个选项是使用索引:创建第二个文件,并在该索引文件的每一行中写入数据文件中该行的字节索引。访问数据文件现在涉及两个查找操作(跳到line索引,然后跳到index_value数据文件),但它仍然会很快。加:将节省磁盘空间,因为行可以有不同的长度。减号:您无法使用编辑器触摸数据文件。

另一种选择:(我想我会选择这个)是只使用一个文件,但每一行都以行号和某种分隔符开头。(例如4005:我的数据线)。现在您可以使用二进制搜索的修改版本https://en.wikipedia.org/wiki/Binary_search_algorithm来寻找您的线路。这将采取log(n)寻找操作,其中 n 是总行数。另外:您可以编辑文件,与固定长度的行相比,它可以节省空间。而且它仍然非常快。即使对于一百万行,这也只是大约 20 次立即发生的查找操作。减号:这些可能性中最复杂的一种。(但做起来很有趣;)

编辑:另一种解决方案:将您的文件分成许多较小的文件。如果您有很长的“行”,则每个文件可能只有一行。但随后我会将它们分组放在文件夹中,例如 4/0/05。但是,即使使用较短的行将您的文件分成 - 让我们粗略地说 - 1mb 块,将它们命名为 1000.txt、2000.txt 并读取完全匹配您的行的一个(或两个)应该非常快,很容易实现。

于 2013-10-04T20:24:07.847 回答
1

我遇到了与上面的帖子类似的问题,但是,上面发布的解决方案在我的特定场景中存在问题;该文件对于 linecache 来说太大了,islice 还远远不够快。我想提供第三种(或第四种)替代解决方案。

我的解决方案是基于我们可以使用 mmap 访问文件中的特定点这一事实。我们只需要知道文件中行的开始和结束位置,然后 mmap 就可以以与 linecache 一样快的速度将这些信息提供给我们。要优化此代码(请参阅更新):

  • 我们使用集合中的 deque 类来创建动态长度的端点集合。
  • 然后,我们将其转换为优化对该集合的随机访问的列表。

以下是该过程的简单包装器:

from collections import deque
import mmap

class fast_file():
    
    def __init__(self, file):
        self.file = file
        self.linepoints = deque()
        self.linepoints.append(0)
        pos = 0
        with open(file,'r') as fp:
            while True:
                c = fp.read(1)
                if not c:
                    break
                if c == '\n':
                    self.linepoints.append(pos)
                    pos += 1
                pos += 1
        self.fp = open(self.file,'r+b')
        self.mm = mmap.mmap(self.fp.fileno(),0 )
        self.linepoints.append(pos)
        self.linepoints = list(self.linepoints)
                
    def getline(self, i):
        return self.mm[self.linepoints[i]:self.linepoints[i+1]]          
    
    def close(self):
        self.fp.close()
        self.mm.close()

需要注意的是,文件 mmap 需要关闭,并且枚举端点可能需要一些时间。但这是一次性费用。结果是实例化和随机文件访问都很快,但是,输出是字节类型的元素。

我通过查看前 100 万行(共 4800 万行)访问我的大文件的样本来测试速度。我运行以下命令来了解进行 1000 万次访问所需的时间:

linecache.getline("sample.txt",0)
F = fast_file("sample.txt")


sleep(1)
start = time()
for i in range(10000000):
    linecache.getline("sample.txt",1000)
print(time()-start)

>>> 6.914520740509033

sleep(1)
start = time()
for i in range(10000000):
    F.getline(1000)
print(time()-start) 

>>> 4.488042593002319

sleep(1)
start = time()
for i in range(10000000):
    F.getline(1000).decode()
print(time()-start) 

>>> 6.825756549835205

它并没有那么快,并且需要一些时间来启动(实际上更长),但是,考虑到我的原始文件对于 linecache 来说太大了。这个简单的包装器允许我对 linecache 无法在我的计算机上执行的行(32Gb RAM)进行随机访问。

我认为现在这可能是 linecache 的最佳更快替代方案(速度可能取决于 i/o 和 RAM 速度),但如果您有办法改进这一点,请添加评论,我会相应地更新解决方案。

更新

我最近用更快的 collections.deque 替换了一个列表。

第二次更新 collections.deque 在追加操作中更快,但是,列表对于随机访问更快,因此,这里从双端队列到列表的转换优化了随机访问时间和实例化。我在此测试中添加了 sleep 并在比较中添加了 decode 函数,因为 mmap 将返回字节以使比较公平。

于 2020-06-23T21:54:28.003 回答