
Fantasma silencioso
Necesito obtener un recuento de líneas de un archivo grande (cientos de miles de líneas) en python. ¿Cuál es la forma más eficiente tanto en términos de memoria como de tiempo?
De momento hago:
def file_len(fname):
with open(fname) as f:
for i, l in enumerate(f):
pass
return i + 1
¿es posible hacerlo mejor?

adán yuval
No puedes conseguir nada mejor que eso.
Después de todo, cualquier solución tendrá que leer el archivo completo, averiguar cuántos \n
tienes, y devolver ese resultado.
¿Tiene una mejor manera de hacerlo sin leer todo el archivo? No estoy seguro… La mejor solución siempre será la E/S, lo mejor que puedes hacer es asegurarte de no usar memoria innecesaria, pero parece que lo tienes cubierto.

miguel tocino
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 esto se ejecute considerablemente más rápido, a saber, mediante el uso de la interfaz sin búfer (sin procesar), 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 usarse o no de manera predeterminada, pero en Python 3, usará Unicode de manera 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 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 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

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

nombre
Aquí hay un programa de Python para usar la biblioteca de multiprocesamiento para distribuir el conteo de líneas entre máquinas/núcleos. Mi prueba mejora el conteo de un archivo de 20 millones de líneas de 26 segundos a 7 segundos usando un servidor Windows 64 de 8 núcleos. Nota: no usar el mapeo 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 ) )
¿Necesita un recuento de líneas exacto o será suficiente una aproximación?
– pico
11 de mayo de 2009 a las 20:14
Agregaría i=-1 antes del ciclo for, ya que este código no funciona para archivos vacíos.
– Maciek Sawicki
27 de diciembre de 2011 a las 16:13
@Legend: apuesto a que pico está pensando, obtenga el tamaño del archivo (con seek (0,2) o equivalente), divídalo por la longitud aproximada de la línea. Podrías leer algunas líneas al principio para adivinar la longitud promedio de las líneas.
– Ana
7 febrero 2012 a las 17:02
enumerate(f, 1)
y deshacerse de lai + 1
?–Ian Mackinnon
21 de febrero de 2013 a las 12:25
@IanMackinnon funciona para archivos vacíos, pero debe inicializar I para 0 antes del bucle for.
– scai
13 de agosto de 2013 a las 16:29