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

10 投票
3 回答
4504 浏览
提问于 2025-04-15 23:38

假设我经常需要处理一些行数不确定但很大的文件。每一行都包含一组整数(用空格、逗号、分号或者其他非数字字符分隔),这些整数的范围在[0, R]之间,R可以非常大。每一行的整数个数可能不同。通常情况下,每一行的整数个数是相同的,但有时也会有行的整数个数不一样。

假设我想要访问文件中的第N行,并获取该行的第K个数字(假设N和K都是有效的输入——也就是说,我不担心输入错误)。我该如何在Windows上使用Python 3.1.2高效地做到这一点呢?

我不想逐行读取文件。

我尝试过使用mmap,但在这里浏览StackOverflow时,我了解到在32位系统上这可能不是最佳解决方案,因为有4GB的限制。实际上,我也搞不清楚如何简单地从当前位置跳到第N行。如果我能至少“跳”到第N行,那么我就可以用.split()方法来获取第K个整数。

这里的细节是,我不仅仅需要从文件中获取一行。我需要获取几行:这些行不一定都靠近彼此,我获取它们的顺序很重要,而且这个顺序并不总是基于某个确定的函数。

有什么想法吗?希望这些信息足够了。

谢谢!

3 个回答

0

另一种解决方案是,如果这个文件可能会频繁变化,直接使用一个合适的数据库。数据库系统会为你创建和维护索引,这样你就可以进行非常快速的搜索和查询。

不过,这样做可能有点过于复杂了。

4

问题在于,由于你的行不是固定长度的,所以你需要注意行结束的标记来进行查找。这实际上就变成了“逐行读取文件”。因此,任何可行的方法最终还是要逐行读取文件,关键在于哪个方法能更快地完成这个过程。

17

Python中的seek方法是用来在文件中定位到一个字节的偏移量,而不是的偏移量。这是因为现代操作系统和文件系统的工作方式就是这样——它们根本不记录“行偏移量”,所以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
$ 

示例文件是《圣经·钦定本》的纯文本文件:

$ wc kjv10.txt
100117  823156 4445260 kjv10.txt

有10万行,4.4MB,正如你所看到的;索引大约需要四分之一秒,读取并打印三行随机的内容大约需要50毫秒(毫无疑问,通过更仔细的优化和更好的机器,这个速度可以大大提升)。内存中的索引(以及磁盘上的索引)每行文本文件占用4个字节,性能应该是完全线性的,所以如果你有大约1亿行,4.4GB,我预计构建索引大约需要4-5分钟,提取并打印三行任意内容需要1分钟(而索引占用的400MB内存对于小机器来说也不会造成困扰——毕竟我这台小慢笔记本也有2GB内存;-)。

你还可以看到(为了速度和方便),我将文件视为二进制文件(并假设使用utf8编码——当然也可以处理任何子集,比如ASCII,KJ文本文件就是ASCII),而且我不在意将\r\n合并成一个字符,如果文件的行结束符是这样的话(如果你想的话,读取每行后再处理成一个字符也是很简单的)。

撰写回答