如何在 Python 中廉价地获取大文件的行数?

Posted

技术标签:

【中文标题】如何在 Python 中廉价地获取大文件的行数?【英文标题】:How to get line count of a large file cheaply in Python? 【发布时间】:2010-10-25 02:36:44 【问题描述】:

我需要在 python 中获取一个大文件(数十万行)的行数。在内存和时间方面最有效的方法是什么?

目前我这样做:

def file_len(fname):
    with open(fname) as f:
        for i, l in enumerate(f):
            pass
    return i + 1

有没有可能做得更好?

【问题讨论】:

您需要精确的行数还是一个近似值就足够了? 我会在 for 循环之前添加 i=-1,因为此代码不适用于空文件。 @Legend:我敢打赌 pico 正在考虑,获取文件大小(使用 seek(0,2) 或等值),除以近似行长度。您可以在开头阅读几行来猜测平均行长。 enumerate(f, 1) 并放弃 i + 1? @IanMackinnon 适用于空文件,但您必须在 for 循环之前将 i 初始化为 0 【参考方案1】:

你可以执行一个子进程并运行wc -l filename

import subprocess

def file_len(fname):
    p = subprocess.Popen(['wc', '-l', fname], stdout=subprocess.PIPE, 
                                              stderr=subprocess.PIPE)
    result, err = p.communicate()
    if p.returncode != 0:
        raise IOError(err)
    return int(result.strip().split()[0])

【讨论】:

这个的windows版本是什么? 你可以参考这个 SO question 。 ***.com/questions/247234/… 确实,在我的情况下(Mac OS X),计算“for x in file(...)”产生的行数需要 0.13 秒而不是 0.5 秒,而计算重复调用需要 1.0 秒到 str.find 或 mmap.find。 (我用来测试的文件有 130 万行。) 不需要涉及到外壳。编辑答案并添加示例代码; 不是跨平台的。【参考方案2】:
def file_len(full_path):
  """ Count number of lines in a file."""
  f = open(full_path)
  nr_of_lines = sum(1 for line in f)
  f.close()
  return nr_of_lines

【讨论】:

命令“sum(1 for line in f)”似乎删除了文件的内容。如果我将命令“f.readline()”放在该行之后,则返回 null。【参考方案3】:

没有比这更好的了。

毕竟,任何解决方案都必须读取整个文件,计算出您有多少\n,然后返回该结果。

您有没有更好的方法来做到这一点而无需阅读整个文件?不确定...最好的解决方案始终是 I/O-bound,你能做的最好的就是确保你不使用不必要的内存,但看起来你已经涵盖了。

【讨论】:

没错,即使 WC 也在读取文件,但在 C 中,它可能已经非常优化了。 据我了解,Python 文件 IO 也是通过 C 完成的。 docs.python.org/library/stdtypes.html#file-objects @Tomalak 这是一条红鲱鱼。虽然 python 和 wc 可能发出相同的系统调用,但 python 具有 wc 没有的操作码调度开销。 您可以通过采样来近似行数。它可以快数千倍。见:documentroot.com/2011/02/… 其他答案似乎表明这个分类答案是错误的,因此应该删除而不是保留为已接受。【参考方案4】:

对我来说,这个变种将是最快的:

#!/usr/bin/env python

def main():
    f = open('filename')                  
    lines = 0
    buf_size = 1024 * 1024
    read_f = f.read # loop optimization

    buf = read_f(buf_size)
    while buf:
        lines += buf.count('\n')
        buf = read_f(buf_size)

    print lines

if __name__ == '__main__':
    main()

原因:缓冲比逐行读取快,string.count也很快

【讨论】:

但是是这样吗?根据 timeit.py,至少在 OSX/python2.5 上,OP 的版本仍然快 10% 左右。 如果最后一行没有以'\n'结尾怎么办? 我不知道你是如何测试它的,dF,但在我的机器上它比任何其他选项慢约 2.5 倍。 你说它会是最快的,然后说你没有测试过。不是很科学吧? :) 查看下面 Ryan Ginstrom 回答提供的解决方案和统计数据。另请查看 JF Sebastian 对同一答案的评论和链接。【参考方案5】:

打开文件的结果是一个迭代器,可以转换成一个序列,有一个长度:

with open(filename) as f:
   return len(list(f))

这比你的显式循环更简洁,并且避免了enumerate

【讨论】:

这意味着需要将 100 Mb 的文件读入内存。 是的,好点,虽然我想知道速度(相对于内存)的差异。可能可以创建一个执行此操作的迭代器,但我认为它等同于您的解决方案。 -1,不只是内存,还得在内存中构造列表。【参考方案6】:

这个呢

def file_len(fname):
  counts = itertools.count()
  with open(fname) as f: 
    for _ in f: counts.next()
  return counts.next()

【讨论】:

【参考方案7】:

为什么不读取前 100 行和后 100 行并估计平均行长度,然后将总文件大小除以这些数字?如果您不需要确切的值,这可以工作。

【讨论】:

我需要一个准确的值,但问题是在一般情况下,行长度可能会有很大的不同。恐怕您的方法不会是最有效的方法。【参考方案8】:

我相信内存映射文件将是最快的解决方案。我尝试了四个功能:OP发布的功能(opcount);对文件中的行进行简单的迭代 (simplecount);带有内存映射文件 (mmap) (mapcount) 的 readline;以及 Mykola Kharechko (bufcount) 提供的缓冲区读取解决方案。

我对每个函数运行了五次,并计算了一个 120 万行文本文件的平均运行时间。

Windows XP、Python 2.5、2GB RAM、2 GHz AMD 处理器

这是我的结果:

mapcount : 0.465599966049
simplecount : 0.756399965286
bufcount : 0.546800041199
opcount : 0.718600034714

编辑:Python 2.6 的数字:

mapcount : 0.471799945831
simplecount : 0.634400033951
bufcount : 0.468800067902
opcount : 0.602999973297

所以缓冲区读取策略似乎是 Windows/Python 2.6 中最快的

代码如下:

from __future__ import with_statement
import time
import mmap
import random
from collections import defaultdict

def mapcount(filename):
    f = open(filename, "r+")
    buf = mmap.mmap(f.fileno(), 0)
    lines = 0
    readline = buf.readline
    while readline():
        lines += 1
    return lines

def simplecount(filename):
    lines = 0
    for line in open(filename):
        lines += 1
    return lines

def bufcount(filename):
    f = open(filename)                  
    lines = 0
    buf_size = 1024 * 1024
    read_f = f.read # loop optimization

    buf = read_f(buf_size)
    while buf:
        lines += buf.count('\n')
        buf = read_f(buf_size)

    return lines

def opcount(fname):
    with open(fname) as f:
        for i, l in enumerate(f):
            pass
    return i + 1


counts = defaultdict(list)

for i in range(5):
    for func in [mapcount, simplecount, bufcount, opcount]:
        start_time = time.time()
        assert func("big_file.txt") == 1209138
        counts[func].append(time.time() - start_time)

for key, vals in counts.items():
    print key.__name__, ":", sum(vals) / float(len(vals))

【讨论】:

看来wccount()是最快的gist.github.com/0ac760859e614cd03652 缓冲读取是最快的解决方案,而不是mmapwccount。见***.com/a/68385697/353337。【参考方案9】:

一行,可能很快:

num_lines = sum(1 for line in open('myfile.txt'))

【讨论】:

它类似于 sum(sequence of 1) 每行都计为 1。 >>> [ 1 for line in range(10) ] [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] >>> sum( 1 for line in range(10) ) 10 >>> num_lines = sum(1 for line in open('myfile.txt') if line.rstrip()) for filter empty lines 当我们打开一个文件时,一旦我们遍历所有元素,它会自动关闭吗?是否需要“关闭()”?我认为我们不能在这个简短的声明中使用“with open()”,对吧? 轻微的 lint 改进:num_lines = sum(1 for _ in open('myfile.txt')) 它并不比其他解决方案快,请参阅***.com/a/68385697/353337。【参考方案10】:

为了完成上述方法,我尝试了 fileinput 模块的变体:

import fileinput as fi   
def filecount(fname):
        for line in fi.input(fname):
            pass
        return fi.lineno()

并将一个 6000 万行的文件传递给上述所有方法:

mapcount : 6.1331050396
simplecount : 4.588793993
opcount : 4.42918205261
filecount : 43.2780818939
bufcount : 0.170812129974

令我有点惊讶的是,fileinput 如此糟糕,而且规模远比所有其他方法都要糟糕...

【讨论】:

【参考方案11】:

这个呢?

import sys
sys.stdin=open('fname','r')
data=sys.stdin.readlines()
print "counted",len(data),"lines"

【讨论】:

我不认为它解决了大文件被读入内存的事实。 打印 "counted",len(data),"lines" ^ SyntaxError: invalid syntax【参考方案12】:

为什么以下方法不起作用?

import sys

# input comes from STDIN
file = sys.stdin
data = file.readlines()

# get total number of lines in file
lines = len(data)

print lines

在这种情况下,len 函数使用输入行作为确定长度的方法。

【讨论】:

问题不是如何获得行数,我已经在问题本身中证明了我在做什么:问题是如何有效地做到这一点。在您的解决方案中,将整个文件读入内存,这对于大文件来说至少效率低下,对于大文件来说最多是不可能的。 实际上它可能非常有效,除非它不可能。 :-)【参考方案13】:

count = max(enumerate(open(filename)))[0]

【讨论】:

这给出了真值的计数-1。 enumerate() 的可选第二个参数是根据docs.python.org/2/library/functions.html#enumerate 的开始计数【参考方案14】:

这个怎么样?

import fileinput
import sys

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

fileinput.close()
print counter

【讨论】:

【参考方案15】:

这是一个 python 程序,它使用多处理库在机器/内核之间分配行数。我的测试使用 8 核 windows 64 服务器将 2000 万行文件的计数从 26 秒提高到 7 秒。注意:不使用内存映射会使事情变得更慢。

import multiprocessing, sys, time, os, mmap
import logging, logging.handlers

def init_logger(pid):
    console_format = 'P0 %(levelname)s %(message)s'.format(pid)
    logger = logging.getLogger()  # New logger at root level
    logger.setLevel( logging.INFO )
    logger.handlers.append( logging.StreamHandler() )
    logger.handlers[0].setFormatter( logging.Formatter( console_format, '%d/%m/%y %H:%M:%S' ) )

def getFileLineCount( queues, pid, processes, file1 ):
    init_logger(pid)
    logging.info( 'start' )

    physical_file = open(file1, "r")
    #  mmap.mmap(fileno, length[, tagname[, access[, offset]]]

    m1 = mmap.mmap( physical_file.fileno(), 0, access=mmap.ACCESS_READ )

    #work out file size to divide up line counting

    fSize = os.stat(file1).st_size
    chunk = (fSize / processes) + 1

    lines = 0

    #get where I start and stop
    _seedStart = chunk * (pid)
    _seekEnd = chunk * (pid+1)
    seekStart = int(_seedStart)
    seekEnd = int(_seekEnd)

    if seekEnd < int(_seekEnd + 1):
        seekEnd += 1

    if _seedStart < int(seekStart + 1):
        seekStart += 1

    if seekEnd > fSize:
        seekEnd = fSize

    #find where to start
    if pid > 0:
        m1.seek( seekStart )
        #read next line
        l1 = m1.readline()  # need to use readline with memory mapped files
        seekStart = m1.tell()

    #tell previous rank my seek start to make their seek end

    if pid > 0:
        queues[pid-1].put( seekStart )
    if pid < processes-1:
        seekEnd = queues[pid].get()

    m1.seek( seekStart )
    l1 = m1.readline()

    while len(l1) > 0:
        lines += 1
        l1 = m1.readline()
        if m1.tell() > seekEnd or len(l1) == 0:
            break

    logging.info( 'done' )
    # add up the results
    if pid == 0:
        for p in range(1,processes):
            lines += queues[0].get()
        queues[0].put(lines) # the total lines counted
    else:
        queues[0].put(lines)

    m1.close()
    physical_file.close()

if __name__ == '__main__':
    init_logger( 'main' )
    if len(sys.argv) > 1:
        file_name = sys.argv[1]
    else:
        logging.fatal( 'parameters required: file-name [processes]' )
        exit()

    t = time.time()
    processes = multiprocessing.cpu_count()
    if len(sys.argv) > 2:
        processes = int(sys.argv[2])
    queues=[] # a queue for each process
    for pid in range(processes):
        queues.append( multiprocessing.Queue() )
    jobs=[]
    prev_pipe = 0
    for pid in range(processes):
        p = multiprocessing.Process( target = getFileLineCount, args=(queues, pid, processes, file_name,) )
        p.start()
        jobs.append(p)

    jobs[0].join() #wait for counting to finish
    lines = queues[0].get()

    logging.info( 'finished  Lines:'.format( time.time() - t, lines ) )

【讨论】:

如何处理比主内存大得多的文件?例如,在具有 4GB RAM 和 2 个内核的系统上的 20GB 文件 现在很难测试,但我认为它会将文件分页进出。 这是非常简洁的代码。我惊讶地发现使用多个处理器更快。我认为 IO 将成为瓶颈。在较旧的 Python 版本中,第 21 行需要 int(),例如 chunk = int((fSize / processes)) + 1 它会将所有文件加载到内存中吗?比电脑上的内存还大的大火呢? 文件被映射到虚拟内存中,因此文件的大小和实际内存的大小通常不受限制。【参考方案16】:

我已经像这样修改了缓冲区案例:

def CountLines(filename):
    f = open(filename)
    try:
        lines = 1
        buf_size = 1024 * 1024
        read_f = f.read # loop optimization
        buf = read_f(buf_size)

        # Empty file
        if not buf:
            return 0

        while buf:
            lines += buf.count('\n')
            buf = read_f(buf_size)

        return lines
    finally:
        f.close()

现在也计算空文件和最后一行(不带 \n)。

【讨论】:

也许还可以解释(或在代码中添加注释)您更改的内容和用途;)。可能会让人们更容易地了解您的代码(而不是“解析”大脑中的代码)。 循环优化我认为允许 Python 在 read_f 处进行局部变量查找,python.org/doc/essays/list2str【参考方案17】:

我在这个版本中获得了一个小的 (4-8%) 改进,它重用了一个常量缓冲区,因此它应该避免任何内存或 GC 开销:

lines = 0
buffer = bytearray(2048)
with open(filename) as f:
  while f.readinto(buffer) > 0:
      lines += buffer.count('\n')

您可以调整缓冲区大小,也许会看到一些改进。

【讨论】:

不错。要考虑不以 \n 结尾的文件,请在循环外添加 1 if buffer and buffer[-1]!='\n' 一个bug:上一轮的缓冲区可能不干净。 如果在缓冲区之间一部分以\结尾而另一部分以n开头怎么办?那会错过一个新行,我会建议变量来存储每个块的结束和开始,但这可能会增加脚本的时间 =(【参考方案18】:

同样:

lines = 0
with open(path) as f:
    for line in f:
        lines += 1

【讨论】:

【参考方案19】:

这个单线怎么样:

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

【讨论】:

【参考方案20】:

我会使用Python的文件对象方法readlines,如下:

with open(input_file) as foo:
    lines = len(foo.readlines())

这将打开文件,在文件中创建行列表,计算列表的长度,将其保存到变量并再次关闭文件。

【讨论】:

虽然这是首先想到的方法之一,但它可能不是非常节省内存,尤其是在计算高达 10 GB 的文件中的行数时(像我一样),这是值得注意的劣势。 @TimeSheep 对于具有许多(例如,数十亿)小行的文件或具有极长行(例如,每行千兆字节)的文件,这是一个问题吗? 我问的原因是,编译器似乎应该能够通过不创建中间列表来优化它。 @dmityugov 根据 Python 文档,xreadlines 自 2.3 以来已被弃用,因为它只返回一个迭代器。 for line in file 是指定的替换。见:docs.python.org/2/library/stdtypes.html#file.xreadlines【参考方案21】:
print open('file.txt', 'r').read().count("\n") + 1

【讨论】:

【参考方案22】:
def line_count(path):
    count = 0
    with open(path) as lines:
        for count, l in enumerate(lines, start=1):
            pass
    return count

【讨论】:

【参考方案23】:

如果想在 Linux 中用 Python 廉价地获取行数,我推荐这种方法:

import os
print os.popen("wc -l file_path").readline().split()[0]

file_path 既可以是抽象文件路径,也可以是相对路径。希望这可能会有所帮助。

【讨论】:

【参考方案24】:

Kyle's answer

num_lines = sum(1 for line in open('my_file.txt'))

可能是最好的,替代方法是

num_lines =  len(open('my_file.txt').read().splitlines())

这是两者的性能比较

In [20]: timeit sum(1 for line in open('Charts.ipynb'))
100000 loops, best of 3: 9.79 µs per loop

In [21]: timeit len(open('Charts.ipynb').read().splitlines())
100000 loops, best of 3: 12 µs per loop

【讨论】:

【参考方案25】:

您可以通过以下方式使用os.path 模块:

import os
import subprocess
Number_lines = int( (subprocess.Popen( 'wc -l 0'.format( Filename ), shell=True, stdout=subprocess.PIPE).stdout).readlines()[0].split()[0] )

,其中Filename 是文件的绝对路径。

【讨论】:

这个答案和os.path有什么关系?【参考方案26】:

我不得不在一个类似的问题上发布这个,直到我的声誉得分有所上升(感谢撞到我的人!)。

所有这些解决方案都忽略了一种使该程序运行得更快的方法,即使用无缓冲(原始)接口、使用字节数组和进行自己的缓冲。 (这只适用于 Python 3。在 Python 2 中,默认情况下可能使用也可能不使用原始接口,但在 Python 3 中,您将默认使用 Unicode。)

使用计时工具的修改版本,我相信以下代码比提供的任何解决方案都更快(并且稍微更 Python 化):

def rawcount(filename):
    f = open(filename, 'rb')
    lines = 0
    buf_size = 1024 * 1024
    read_f = f.raw.read

    buf = read_f(buf_size)
    while buf:
        lines += buf.count(b'\n')
        buf = read_f(buf_size)

    return lines

使用单独的生成器函数,运行速度更快:

def _make_gen(reader):
    b = reader(1024 * 1024)
    while b:
        yield b
        b = reader(1024*1024)

def rawgencount(filename):
    f = open(filename, 'rb')
    f_gen = _make_gen(f.raw.read)
    return sum( buf.count(b'\n') for buf in f_gen )

这可以通过使用 itertools 内联的生成器表达式来完成,但是看起来很奇怪:

from itertools import (takewhile,repeat)

def rawincount(filename):
    f = open(filename, 'rb')
    bufgen = takewhile(lambda x: x, (f.raw.read(1024*1024) for _ in repeat(None)))
    return sum( buf.count(b'\n') for buf in bufgen )

这是我的时间安排:

function      average, s  min, s   ratio
rawincount        0.0043  0.0041   1.00
rawgencount       0.0044  0.0042   1.01
rawcount          0.0048  0.0045   1.09
bufcount          0.008   0.0068   1.64
wccount           0.01    0.0097   2.35
itercount         0.014   0.014    3.41
opcount           0.02    0.02     4.83
kylecount         0.021   0.021    5.05
simplecount       0.022   0.022    5.25
mapcount          0.037   0.031    7.46

【讨论】:

我正在处理 100Gb+ 的文件,而您的 rawgencounts 是我迄今为止看到的唯一可行的解​​决方案。谢谢! 在此表中是 wccount 用于子进程 shell wc 工具吗? 感谢@michael-bacon,这是一个非常好的解决方案。您可以通过使用bufgen = iter(partial(f.raw.read, 1024*1024), b'') 而不是结合takewhilerepeat 来使rawincount 解决方案看起来不那么奇怪。 哦,部分功能,是的,这是一个不错的小调整。另外,我假设 1024*1024 会被解释器合并并被视为一个常量,但这是基于预感而不是文档。 @MichaelBacon,用buffering=0打开文件然后调用read而不是仅仅以“rb”打开文件并调用raw.read会更快,还是会优化到同样的事情?【参考方案27】:

另一种可能性:

import subprocess

def num_lines_in_file(fpath):
    return int(subprocess.check_output('wc -l %s' % fpath, shell=True).strip().split()[0])

【讨论】:

【参考方案28】:

这段代码更短更清晰。这可能是最好的方法:

num_lines = open('yourfile.ext').read().count('\n')

【讨论】:

你也应该关闭文件。 它将整个文件加载到内存中。【参考方案29】:

这是我使用纯 python 发现的最快的东西。 您可以通过设置缓冲区来使用所需的任何内存量,尽管 2**16 似乎是我计算机上的最佳选择。

from functools import partial

buffer=2**16
with open(myfile) as f:
        print sum(x.count('\n') for x in iter(partial(f.read,buffer), ''))

我在这里找到了答案 Why is reading lines from stdin much slower in C++ than Python? 并稍微调整了一下。虽然wc -l 仍然比其他任何东西快 75% 左右,但它是一本很好的读物,可以了解如何快速计算行数。

【讨论】:

【参考方案30】:

一线解决方案:

import os
os.system("wc -l  filename")  

我的sn-p:

>>> os.system('wc -l *.txt')

0 bar.txt
1000 command.txt
3 test_file.txt
1003 total

【讨论】:

好主意,但不幸的是,这在 Windows 上不起作用。 如果你想成为python的冲浪者,就和windows说再见吧。相信我,你总有一天会感谢我的。 我只是认为值得注意的是,这仅适用于 Windows。我更喜欢自己在 linux/unix 堆栈上工作,但是在编写软件时,恕我直言,应该考虑程序在不同操作系统下运行时可能产生的副作用。由于 OP 没有提及他的平台,并且如果有人通过 google 弹出该解决方案并复制它(不知道 Windows 系统可能存在的限制),我想添加注释。 您无法将os.system() 的输出保存到变量并进行后处理。 @AnSe 你是对的,但没有询问是否保存的问题。我想你正在理解上下文。

以上是关于如何在 Python 中廉价地获取大文件的行数?的主要内容,如果未能解决你的问题,请参考以下文章

插入大查询表的行数少于预期

Python - 如何获取文本文件中的行数[重复]

如何有效地计算数据帧的行数? [复制]

转载python计算文件的行数和读取某一行内容的实现方法

读取大文本文件VB6中的行数

计算非常大文件中的行数会导致 System OutofMemory 异常 [重复]