Pregunta ¿Cómo obtener el recuento de líneas a bajo precio en Python?


Necesito obtener un conteo de líneas de un archivo grande (cientos de miles de líneas) en Python. ¿Cuál es la forma más eficiente tanto de memoria como de tiempo?

Por el momento, lo hago:

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

¿Es posible hacer algo mejor?


721
2018-05-10 10:22


origen


Respuestas:


No se puede obtener nada mejor que eso.

Después de todo, cualquier solución tendrá que leer todo el archivo, averiguar cuántos \n tienes, y devuelve ese resultado.

¿Tienes una mejor manera de hacerlo sin leer todo el archivo? No estoy seguro ... La mejor solución siempre estará ligada a E / S, lo mejor que puede hacer es asegurarse de no utilizar memoria innecesaria, pero parece que tiene eso cubierto.


244
2018-05-10 10:37



Una línea, probablemente bastante rápido:

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

468
2018-06-19 19:07



Creo que un archivo mapeado en memoria será la solución más rápida. Intenté cuatro funciones: la función publicada por el OP (opcount); una simple iteración sobre las líneas en el archivo (simplecount); readline con un archivo mapeado en memoria (mmap) (mapcount); y la solución de lectura de tampón ofrecida por Mykola Kharechko (bufcount)

Ejecuté cada función cinco veces y calculé el tiempo de ejecución promedio para un archivo de texto de 1,2 millones de líneas.

Windows XP, Python 2.5, 2GB de RAM, procesador AMD de 2 GHz

Aquí están mis resultados:

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

Editar: números para Python 2.6:

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

Entonces, la estrategia de lectura del buffer parece ser la más rápida para Windows / Python 2.6

Aquí está el código:

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))

177
2018-05-12 02:49



Podría ejecutar un subproceso y ejecutar 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])

70
2018-05-10 10:28



Tuve que publicar esto en una pregunta similar hasta que mi puntaje de reputación saltó un poco (¡gracias a quien me golpeó!).

Todas estas soluciones ignoran una forma de hacer que esta ejecución sea considerablemente más rápida, es decir, mediante el uso de la interfaz sin búfer (en bruto), el uso de bytearrays, y haciendo su propio almacenamiento en búfer. (Esto solo se aplica en Python 3. En Python 2, la interfaz sin procesar puede o no ser utilizada de manera predeterminada, pero en Python 3, se utilizará de manera predeterminada en Unicode).

Usando una versión modificada de la herramienta de sincronización, creo que el siguiente código es más rápido (y marginalmente más pitónico) que cualquiera de las soluciones ofrecidas:

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

Usando una función de generador separada, esto ejecuta un smidge más rápido:

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 )

Esto se puede hacer completamente con expresiones de generadores en línea usando itertools, pero se vuelve bastante extraño:

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 )

Aquí están mis horarios:

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

70
2017-12-17 04:32



Aquí hay un programa de Python para usar la biblioteca de multiproceso para distribuir el recuento de líneas entre máquinas / núcleos. Mi prueba mejora el conteo de un archivo de línea de 20 millones de 26 segundos a 7 segundos usando un servidor de Windows 8 de 8 núcleos. Nota: el no usar la asignación de memoria hace que las cosas sean mucho más lentas.

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

def init_logger(pid):
    console_format = 'P{0} %(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 ) )

28
2017-07-26 06:51



Usaría el método de objeto de archivo de Python readlines, como sigue:

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

Esto abre el archivo, crea una lista de líneas en el archivo, cuenta la longitud de la lista, la guarda en una variable y cierra el archivo nuevamente.


12
2017-10-08 12:46