Cómo obtener el recuento de líneas de un archivo grande de forma económica en Python
¿Cómo puedo obtener un recuento de líneas de un archivo grande de la manera más eficiente en cuanto a memoria y tiempo?
def file_len(filename):
with open(filename) as f:
for i, _ in enumerate(f):
pass
return i + 1
Una línea, más rápida que el for
bucle del OP (aunque no la más rápida) y muy concisa:
num_lines = sum(1 for _ in open('myfile.txt'))
También puedes aumentar la velocidad (y la robustez) usando rbU
el modo e incluirlo en un with
bloque para cerrar el archivo:
with open("myfile.txt", "rbU") as f:
num_lines = sum(1 for _ in f)
Nota : El modo U
in rbU
está obsoleto desde Python 3.3 y versiones posteriores, por lo que deberíamos usarlo rb
en lugar de rbU
(y se eliminó en Python 3.11 ).
No hay nada mejor que eso.
Después de todo, cualquier solución tendrá que leer el archivo completo, calcular cuántos \n
tiene y devolver ese resultado.
¿Tiene una mejor manera de hacerlo sin leer el archivo completo? No estoy seguro... La mejor solución siempre será la de E/S, lo mejor que puedes hacer es asegurarte de no utilizar memoria innecesaria, pero parece que ya lo tienes cubierto.
[Editar mayo de 2023]
Como se comentó en muchas otras respuestas, en Python 3 existen mejores alternativas. El for
bucle no es el más eficiente. Por ejemplo, usar mmap
buffers o es más eficiente.
Creo que un archivo mapeado en memoria será la solución más rápida. Probé cuatro funciones: la función publicada por el OP ( opcount
); una iteración simple sobre las líneas del archivo ( simplecount
); readline con un archivo mapeado en memoria (mmap) ( mapcount
); y la solución de lectura de buffer 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, 2 GB de RAM, procesador AMD de 2 GHz
Aquí están mis resultados:
mapcount : 0.465599966049
simplecount : 0.756399965286
bufcount : 0.546800041199
opcount : 0.718600034714
Números para Python 2.6:
mapcount : 0.471799945831
simplecount : 0.634400033951
bufcount : 0.468800067902
opcount : 0.602999973297
Entonces, la estrategia de lectura del búfer 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):
with open(filename, "r+") as f:
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))
Todas estas soluciones ignoran una forma de hacer que esto se ejecute considerablemente más rápido, es decir, utilizando la interfaz sin búfer (sin búfer), usando bytearrays y haciendo su propio almacenamiento en búfer. (Esto solo se aplica en Python 3. En Python 2, la interfaz sin formato puede usarse o no de forma predeterminada, pero en Python 3, se utilizará Unicode de forma predeterminada).
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 Pythonic) 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 se ejecuta un poco 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 generadores de expresiones en línea usando itertools, pero se ve 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
Podrías ejecutar un subproceso y ejecutarwc -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])