如何以低廉的价格在Python中获取大文件的行数

1289
如何以最节省内存和时间的方式获取大文件的行数?
def file_len(filename):
    with open(filename) as f:
        for i, _ in enumerate(f):
            pass
    return i + 1

16
您需要精确的行数还是近似值就可以了? - pico
61
由于此代码无法处理空文件,因此我建议在for循环之前添加i = -1。 - Maciek Sawicki
14
@Legend: 我敢打赌Pico正在考虑使用seek(0,2)或相似函数获取文件大小,然后将文件大小除以大约的行长度来计算。你可以读取开始几行来猜测平均行长度。 - Anne
41
enumerate(f, 1) 代替 range(len(f)) 并省略 i + 1 - Ian Mackinnon
6
适用于空文件,但在 for 循环之前必须将 i 初始化为 0 - scai
显示剩余6条评论
45个回答

2
简单方法:
1)
>>> f = len(open("myfile.txt").readlines())
>>> f

430
>>> f = open("myfile.txt").read().count('\n')
>>> f
430
>>>
num_lines = len(list(open('myfile.txt')))

7
在这个例子中,文件没有被关闭。 - Maciej M
1
你为什么提供了3个选项?它们有什么不同?每个选项的优缺点是什么? - Neuron
请添加一个解释。提前谢谢。 - undefined

2
如果想要在Linux中以较低的成本获取Python代码行数,我推荐使用以下方法:
import os
print os.popen("wc -l file_path").readline().split()[0]

file_path 可以是抽象的文件路径或相对路径。希望这能有所帮助。


2
print open('file.txt', 'r').read().count("\n") + 1

2
这是关于其他答案的元评论。
  • 行读取和缓冲的 \n 计数技术不会对每个文件返回相同的答案,因为某些文本文件在最后一行没有换行符。你可以通过检查最后一个非空缓冲区的最后一个字节并在其不是 b'\n' 时添加 1 来解决这个问题。

  • 在 Python 3 中,以文本模式和二进制模式打开文件可能会产生不同的结果,因为文本模式默认将 CR、LF 和 CRLF 识别为行尾(将它们全部转换为 '\n'),而在二进制模式下只有 LF 和 CRLF 会被计算为 b'\n'。无论你是按行读取还是读入固定大小的缓冲区,都是如此。经典的 Mac OS 使用 CR 作为行尾;我不知道这些文件现在有多常见。

  • 缓冲区读取方法使用的 RAM 量是有限的,与文件大小无关,而行读取方法在最坏情况下可能会一次性将整个文件读入 RAM 中(特别是如果文件使用 CR 行尾)。在最坏情况下,它可能会使用比文件大小更多得多的 RAM,因为动态调整行缓冲区大小和(如果你以文本模式打开)Unicode 解码和存储都会带来开销。

  • 你可以通过预先分配 bytearray 并使用 readinto 而不是 read 来改善缓冲区方法的内存使用情况,而且可能还能提高速度。其中一个现有答案(票数较少)就是这样做的,但它有 bug(它会重复计算一些字节)。

  • 顶部的缓冲区读取答案使用了一个大缓冲区(1 MiB)。使用更小的缓冲区实际上可能更快,因为操作系统会进行预读取。如果每次读取 32K 或 64K,操作系统可能会在你请求数据之前开始将下一个 32K/64K 读入缓存中,并且每次访问内核都几乎立即返回。如果每次读取 1 MiB,操作系统不太可能预读整个兆字节。它可能会预读更少的数据,但你仍然需要花费相当长的时间在内核中等待磁盘返回其余的数据。


2

使用Numba

我们可以使用Numba将函数即时编译为机器码。 def numbacountparallel(fname) 运行速度比问题中的def file_len(fname) 快2.8倍。

注意事项:

在基准测试运行之前,操作系统已经将文件缓存到内存中,因此我在我的PC上看不到太多的磁盘活动。当第一次读取文件时,时间会慢得多,使得使用Numba的时间优势微不足道。

JIT编译在第一次调用函数时需要额外的时间。

如果我们要做的不仅仅是计算行数,这将非常有用。

Cython是另一个选择。

http://numba.pydata.org/

结论

由于计算行数将受到IO限制,因此除非您想要执行更多操作而不仅仅是计算行数,否则请使用问题中的def file_len(fname)函数。

import timeit

from numba import jit, prange
import numpy as np

from itertools import (takewhile,repeat)

FILE = '../data/us_confirmed.csv' # 40.6MB, 371755 line file
CR = ord('\n')


# Copied from the question above. Used as a benchmark
def file_len(fname):
    with open(fname) as f:
        for i, l in enumerate(f):
            pass
    return i + 1


# Copied from another answer. Used as a benchmark
def rawincount(filename):
    f = open(filename, 'rb')
    bufgen = takewhile(lambda x: x, (f.read(1024*1024*10) for _ in repeat(None)))
    return sum( buf.count(b'\n') for buf in bufgen )


# Single thread
@jit(nopython=True)
def numbacountsingle_chunk(bs):

    c = 0
    for i in range(len(bs)):
        if bs[i] == CR:
            c += 1

    return c


def numbacountsingle(filename):
    f = open(filename, "rb")
    total = 0
    while True:
        chunk = f.read(1024*1024*10)
        lines = numbacountsingle_chunk(chunk)
        total += lines
        if not chunk:
            break

    return total


# Multi thread
@jit(nopython=True, parallel=True)
def numbacountparallel_chunk(bs):

    c = 0
    for i in prange(len(bs)):
        if bs[i] == CR:
            c += 1

    return c


def numbacountparallel(filename):
    f = open(filename, "rb")
    total = 0
    while True:
        chunk = f.read(1024*1024*10)
        lines = numbacountparallel_chunk(np.frombuffer(chunk, dtype=np.uint8))
        total += lines
        if not chunk:
            break

    return total

print('numbacountparallel')
print(numbacountparallel(FILE)) # This allows Numba to compile and cache the function without adding to the time.
print(timeit.Timer(lambda: numbacountparallel(FILE)).timeit(number=100))

print('\nnumbacountsingle')
print(numbacountsingle(FILE))
print(timeit.Timer(lambda: numbacountsingle(FILE)).timeit(number=100))

print('\nfile_len')
print(file_len(FILE))
print(timeit.Timer(lambda: rawincount(FILE)).timeit(number=100))

print('\nrawincount')
print(rawincount(FILE))
print(timeit.Timer(lambda: rawincount(FILE)).timeit(number=100))

每个函数100次调用所需的秒数

numbacountparallel
371755
2.8007332000000003

numbacountsingle
371755
3.1508585999999994

file_len
371755
6.7945494

rawincount
371755
6.815438

1
def count_text_file_lines(path):
    with open(path, 'rt') as file:
        line_count = sum(1 for _line in file)
    return line_count

1
如果您认为有问题,能否请您解释一下它的问题所在?对我来说它是可以工作的。谢谢! - jciloa
1
我也很想知道为什么这个答案被踩了。它按行迭代文件并将它们加起来。我喜欢它,它简短明了,有什么问题吗? - cessor

1
打开文件的结果是一个迭代器,可以转换为序列,序列具有长度:
with open(filename) as f:
   return len(list(f))

这种方法比你的显式循环更简洁,而且避免了使用enumerate


13
这意味着需要将100 Mb文件读入内存。 - SilentGhost
是的,说得好,虽然我对速度(而不是内存)差异有些疑问。可能可以创建一个迭代器来实现这一点,但我认为这与您的解决方案等效。 - Andrew Jaffe
7
不仅仅是记忆,还需要在记忆中构建列表。 - orip

1

这个怎么样?

import fileinput
import sys

counter=0
for line in fileinput.input([sys.argv[1]]):
    counter+=1

fileinput.close()
print counter

1
这个一行代码怎么样:
file_length = len(open('myfile.txt','r').read().split('\n'))

使用这种方法计时3900行文件只需0.003秒。
def c():
  import time
  s = time.time()
  file_length = len(open('myfile.txt','r').read().split('\n'))
  print time.time() - s

1

这真的比问题中最初的算法更高效吗? - Jean-Francois T.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接