我在文件中有一个ASCII表,我想从中读取一组特定的行(例如,行4003到4005)。问题是这个文件可能非常长(例如100到数千行),我想尽快做到这一点。
错误解决方案:读入整个文件,然后转到这些行,
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吗?
使用二进制文件,在这种情况下可能更容易跳过,是一个选项---但我宁愿避免它。
答案 0 :(得分:18)
我可能只会使用itertools.islice
。在像文件句柄这样的迭代上使用islice意味着整个文件永远不会被读入内存,并且尽可能快地丢弃前4002行。您甚至可以非常便宜地将所需的两条线投射到一个列表中(假设线条本身不是很长)。然后,您可以退出with
块,关闭文件句柄。
from itertools import islice
with open('afile') as f:
lines = list(islice(f, 4003, 4005))
do_something_with(lines)
但是,对于多次访问,圣牛的行速更快。我创建了一个百万行文件来比较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比islice快,但不断重新创建linecache,但是谁做到了?对于可能的场景(只读取几行,一次,读取多行,一次),linecache更快并且呈现简洁的语法,但islice
语法非常干净且快速且无法读取将整个文件存入内存。在RAM紧密的环境中,islice
解决方案可能是正确的选择。对于非常高的速度要求,linecache可能是更好的选择。但实际上,在大多数环境中,两次都足够小,几乎无关紧要。
答案 1 :(得分:7)
这里的主要问题是,换行符与任何其他角色没有任何不同。因此操作系统无法跳过到该行。
那说有几个选择,但每个人都必须以这种或那种方式做出牺牲。
您确实已经说明了第一个:使用二进制文件。如果您有固定的行长,那么您可以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并读取与你的线完全匹配的那一个(或两个)应该很快就很容易实现。
答案 2 :(得分:1)
我遇到了与以上帖子类似的问题,但是,在我的特定情况下,上面发布的解决方案存在问题;该文件对于Linecache太大,而islice的速度还不够快。我想提供第三个(或第四个)替代解决方案。
我的解决方案基于以下事实:我们可以使用mmap访问文件中的特定点。我们只需要知道文件在行中开始和结束的位置,那么mmap可以将它们与行缓存一样快地提供给我们。要优化此代码(请参阅更新):
以下是该过程的简单包装:
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太大。这个简单的包装程序使我可以对行缓存无法在我的计算机(32 Gb RAM)上执行的行进行随机访问。
我认为现在这可能是行高速缓存的最佳替代方案(速度可能取决于I / O和RAM速度),但是如果您有改进的方法,请添加注释,我将相应地更新解决方案。
更新
我最近用一个collections.deque替换了一个列表,速度更快。
第二次更新 在附加操作中,collections.deque更快,但是,列表的随机访问速度更快,因此,此处从双端队列到列表的转换可优化随机访问时间和实例化。我在此测试中添加了睡眠,并在比较中添加了解码功能,因为mmap将返回字节以使比较公平。