读取文件第n行的更快替代方法吗

0 投票
3 回答
762 浏览
提问于 2025-04-15 23:47

我需要从一个特定的行号开始读取文件,我知道这个行号,比如说是“n”:

我想到了两种方法:

1. 第一种方法是用一个循环,从0到n的范围:

for i in range(n):

fname.readline() # 这行代码会读取文件的每一行,直到第n行

k=readline() # 这行代码会读取第n行的内容

print k # 然后打印出这一行的内容

2. 第二种方法是直接遍历文件的每一行:

i=0 # 初始化一个计数器

for line in fname:

dictionary[i]=line # 把每一行的内容存到一个字典里,字典的键是行号

i=i+1 # 每读取一行,计数器加1

不过,我想要一个更快的方法,因为我可能需要对不同的文件执行这个操作20000次。

有没有更好的选择呢?

另外,对于简单的循环,还有其他性能提升的方法吗?因为我的代码里有嵌套循环。

3 个回答

0

如果把文件中每个换行符的位置都缓存起来,会占用很多内存。不过,如果每个内存页(通常是4KB)缓存大约一个位置,就能达到差不多的减少输入输出操作的效果。而从已知位置扫描几KB的数据,其实是没什么成本的。所以,如果你平均每行有40个字符,那你只需要缓存每100个换行符的位置就可以了。具体要缓存到什么程度,取决于你有多少内存和输入输出的速度。其实,你甚至可以只缓存每1000个换行符的位置,性能上也不会有太大的差别。

2

除非你知道或者能算出文件中第 n 行的位置(比如说每行的长度都是固定的),否则你就得一行一行地读,直到找到第 n 行。

关于你的例子:

  • xrangerange 快,因为 range 需要生成一个列表,而 xrange 是用生成器来实现的。
  • 如果你只需要第 n 行,为什么还要把所有的行都存到字典里呢?
5

如果文件不太大,标准库里的 linecache 模块非常好用——它可以让你直接请求某个文件的第 N 行。

如果文件 很大,我建议使用类似下面的方式(注意,这段代码未经测试):

def readlinenum(filepath, n, BUFSIZ=65536):
  bufs = [None] * 2
  previous_lines = lines_so_far = 0
  with open(filepath, 'b') as f
    while True:
      bufs[0] = f.read(BUFSIZ)
      if not bufs[0]:
        raise ValueError('File %s has only %d lines, not %d',
                         filepath, lines_so_far, n)
      lines_this_block = bufs[0].count('\n')
      updated_lines_count = lines_so_far + lines_this_block
      if n < updated_lines_count:
          break
      previous_lines = lines_so_far
      lines_so_far = updated_lines_count
      bufs[1] = bufs[0]
    if n == lines_so_far:
      # line split between blocks
      buf = bufs[1] + bufs[0]
      delta = n - previous_lines
    else:  # normal case
      buf = bufs[0]
      delta = n = lines_so_far
    f = cStringIO.StringIO(buf)
    for i, line in enumerate(f):
      if i == delta: break
    return line.rstrip()

大致的思路是把文件作为二进制文件读取,分成大块(至少要和最长的行一样大)——在 Windows 系统上,从二进制转换成“文本”的处理在大文件上是比较耗时的——然后在大部分块上使用字符串的快速 .count 方法。最后,我们可以在一个单独的块上进行行解析(在极少数情况下,所需的行可能跨越块的边界,最多只需要两个块)。

这种代码需要仔细测试和检查(我在这个例子中没有进行),因为它容易出现越界和其他边界错误,所以我只建议在真正大的文件上使用——那些如果用 linecache 的话,会让内存吃不消的文件(因为 linecache 会把整个文件加载到内存中,而不是按块处理)。例如,在一台典型的现代机器上,如果有 4GB 的内存,我会考虑在文本文件超过 1GB 或 2GB 时使用这种技术。

编辑:有评论者认为在 Windows 上以二进制方式读取文件并没有比文本模式处理快多少。为了证明这个观点是错的,我们可以使用 'U'(“通用换行符”)选项,这样在 Unix 机器上也会进行行尾处理(因为我没有 Windows 机器来测试这个;-)。使用通常的 kjv.txt 文件:

$ wc kjv.txt
  114150  821108 4834378 kjv.txt

(4.8 MB,114 K 行)——大约是我之前提到的文件大小的 1/1000:

$ python -mtimeit 'f=open("kjv.txt", "rb")' 'f.seek(0); f.read()'
100 loops, best of 3: 13.9 msec per loop
$ python -mtimeit 'f=open("kjv.txt", "rU")' 'f.seek(0); f.read()'
10 loops, best of 3: 39.2 msec per loop

也就是说,行尾处理的成本大约正好是 3 倍(这在一台稍旧的笔记本电脑上,但这个 比例 在其他地方应该也差不多)。

当然,逐行读取的速度更慢:

$ python -mtimeit 'f=open("kjv.txt", "rU")' 'f.seek(0)' 'for x in f: pass'
10 loops, best of 3: 54.6 msec per loop

而使用 readline(如评论中提到的,缓冲效率比直接循环读取文件差)是最慢的:

$ python -mtimeit 'f=open("kjv.txt", "rU")' 'f.seek(0); x=1' 'while x: x=f.readline()'
10 loops, best of 3: 81.1 msec per loop

如果像问题中提到的,有 20,000 个文件需要读取(假设它们都比较小,差不多和这个 kjv.txt 一样),最快的方法(以二进制模式一次性读取每个文件)大约需要 260 秒,4-5 分钟,而最慢的方法(基于 readline)大约需要 1600 秒,几乎半个小时——对于很多实际应用来说,这个差异相当明显,我认为大多数情况下都是如此。

撰写回答