【问题标题】:Python fastest access to nth line in huge filePython最快访问文件中的行
【发布时间】:2013-10-11 23:13:29
【问题描述】:

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

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

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

更好的解决方案enumerate 在每一行上,这样它就不会全部在内存中(a lahttps://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

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

【问题讨论】:

  • 你更好的解决方案是好的,但是在第 4005 行之后break;无需继续阅读文件的其余部分。
  • 如果你知道每行的长度,你可以做一个搜索
  • 在这里看看解决方案:stackoverflow.com/a/620492/2770712
  • 如果文件的行有一个已知的长度,你可以很容易地找到你的 #4003 行

标签: python performance file io


【解决方案1】:

我可能只会使用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 可能是更好的选择。但实际上,在大多数环境中,这两个时间都足够小,几乎没有关系。

【讨论】:

  • 谢谢@kojiro,为什么这比linecacheenumerate 更好/更差/不同?
  • @FredrikPihl linecache 针对从文件中读取多行进行了优化。 OP 正在阅读 2。
  • ……但我无法反驳结果。
  • linecache 读取整个文件并在内部缓存它。这可能非常快,特别是如果文件作为一个大块存储在硬盘上。即使是 10mb 的文件也可以在不到 100ms 的时间内读取。如果需要许多随机访问,这会变得更快。
  • 我认为时间是误导性的,因为getline 缓存了整个文件,所以除了第一次运行之外的所有文件都会非常快。只是轶事证据(因为我没有个人资料等),但几天前我使用了你的 islice 解决方案。
【解决方案2】:

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

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

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

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

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

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

【讨论】:

  • 我认为索引文件解决方案是这里最好的主意:)
  • 我也喜欢索引解决方案,但显然它只会对超出某些关键行号的行有所帮助(即,如果目标行号是第一行,则使用索引会更慢)。比较这些方法会很有趣。
  • 感谢@Scheintod,最后一个选项听起来确实很棒!
  • 但是有一个问题:每一行的内容都会有很多数字,尤其是一长串小数...搜索4005: 是否足以防止意外转到4.9340052?
  • 第三种方法的好处是,它可以扩展到整个硬盘大小的文件,而不会明显变慢。所以有空间:)
【解决方案3】:

我遇到了与上述帖子类似的问题,但是,上面发布的解决方案在我的特定场景中存在问题;该文件对于 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 将返回字节以使比较公平。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-06-23
    • 2017-09-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多