在Python中移动到文件中的任意位置

时间:2010-06-06 20:08:40

标签: python file python-3.x

让我们说我经常需要处理具有未知但大量行数的文件。每一行在闭区间[0,R]中包含一组整数(空格,逗号,分号或一些非数字字符是分隔符),其中R可以任意大。每行的整数数量可以变化。通常我会在每一行上获得相同数量的整数,但有时我会使用不等数组的行。

假设我想转到文件中的第N行并检索该行的第K个数字(并假设输入N和K有效 - 也就是说,我不担心输入错误)。我如何在Python 3.1.2 for Windows中有效地执行此操作?

我不想逐行遍历文件。

我尝试使用mmap,但是当我在这里搜索SO时,我了解到这可能不是32位版本的最佳解决方案,因为4GB限制。事实上,我无法弄清楚如何简单地将N行移离当前位置。如果我至少可以“跳转”到第N行,那么我可以使用.split()并以此方式获取Kth整数。

这里的细微差别在于我不需要从文件中抓取一行。我需要抓住几行:它们不一定都是彼此靠近的,我得到它们的顺序很重要,顺序并不总是基于一些确定性函数。

有什么想法吗?我希望这是足够的信息。

谢谢!

3 个答案:

答案 0 :(得分:16)

Python的seek转到文件中的字节偏移量,而不是偏移量,因为这就是现代操作系统及其文件系统的工作方式 - OS / FS只是不以任何方式记录或记住“线路偏移”,并且Python(或任何其他语言)无法神奇地猜测它们。任何声称“走线”的操作都不可避免地需要“遍历文件”(在封面下)以使行号和字节偏移之间的关联。

如果您对此感到满意并且只是希望它隐藏在您的视线之外,那么解决方案就是标准库模块linecache - 但性能不会比您自己编写的代码更好

如果您需要多次从同一个大文件中读取,那么大型优化就是在该大文件上运行一次一个脚本,该脚本构建并保存到磁盘的行号到字节偏移对应(技术上是“索引”辅助文件);然后,所有连续运行(直到大文件更改)可以非常快速地使用索引文件通过大文件以非常高的性能导航。这是你的用例......?

编辑:因为显然这可能适用 - 这是一般的想法(经过仔细测试,错误检查或优化;-)。要制作索引,请使用makeindex.py,如下所示:

import array
import sys

BLOCKSIZE = 1024 * 1024

def reader(f):
  blockstart = 0
  while True:
    block = f.read(BLOCKSIZE)
    if not block: break
    inblock = 0
    while True:
      nextnl = block.find(b'\n', inblock)
      if nextnl < 0:
        blockstart += len(block)
        break
      yield nextnl + blockstart
      inblock = nextnl + 1

def doindex(fn):
  with open(fn, 'rb') as f:
    # result format: x[0] is tot # of lines,
    # x[N] is byte offset of END of line N (1+)
    result = array.array('L', [0])
    result.extend(reader(f))
    result[0] = len(result) - 1
    return result

def main():
  for fn in sys.argv[1:]:
    index = doindex(fn)
    with open(fn + '.indx', 'wb') as p:
      print('File', fn, 'has', index[0], 'lines')
      index.tofile(p)

main()

然后使用它,例如,以下useindex.py

import array
import sys

def readline(n, f, findex):
  f.seek(findex[n] + 1)
  bytes = f.read(findex[n+1] - findex[n])
  return bytes.decode('utf8')

def main():
  fn = sys.argv[1]
  with open(fn + '.indx', 'rb') as f:
    findex = array.array('l')
    findex.fromfile(f, 1)
    findex.fromfile(f, findex[0])
    findex[0] = -1
  with open(fn, 'rb') as f:
    for n in sys.argv[2:]:
      print(n, repr(readline(int(n), f, findex)))

main()

这是一个例子(在我的慢速笔记本电脑上):

$ time py3 makeindex.py kjv10.txt 
File kjv10.txt has 100117 lines

real    0m0.235s
user    0m0.184s
sys 0m0.035s
$ time py3 useindex.py kjv10.txt 12345 98765 33448
12345 '\r\n'
98765 '2:6 But this thou hast, that thou hatest the deeds of the\r\n'
33448 'the priest appointed officers over the house of the LORD.\r\n'

real    0m0.049s
user    0m0.028s
sys 0m0.020s
$ 

示例文件是King James'Saint的纯文本文件:

$ wc kjv10.txt
100117  823156 4445260 kjv10.txt

100K行,4.4 MB,如您所见;这需要大约四分之一秒的索引和50毫秒来读取和打印三条随机y线(毫无疑问,通过更仔细的优化和更好的机器可以大大加速)。内存中的索引(以及磁盘上的索引)每个被索引的文本文件行需要4个字节,性能应该以完全线性的方式扩展,所以如果你有大约1亿行,4.4 GB,我希望大约4-5几分钟来构建索引,一分钟提取并打印出三条任意行(并且为索引采用的400 MB RAM不应该给小机器带来不便 - 即使我的微型慢速笔记本电脑也只有2GB; - )。 / p>

您还可以看到(为了速度和方便)我将文件视为二进制(并假设utf8编码 - 当然也适用于任何子集,如ASCII,例如KJ文本文件是ASCII)并且不要打扰将\r\n折叠为单个字符,如果该文件具有行终止符(如果需要,在读取每一行后那么非常简单)。

答案 1 :(得分:4)

问题在于,由于您的线条长度不固定,因此您必须注意线端标记以进行搜索,并且有效地变为“逐行遍历文件”。因此,任何可行的方法仍然会遍历文件,这仅仅是可以最快地遍历它的问题。

答案 2 :(得分:0)

另一个解决方案,如果文件可能会发生很大变化,那就是完全转向正确的数据库。数据库引擎将为您创建和维护索引,以便您可以进行非常快速的搜索/查询。

但这可能是一种矫枉过正。