ruby - sirve - ¿Cuenta el número de líneas en un archivo sin leer todo el archivo en la memoria?
ruby tutorial (14)
Estoy procesando enormes archivos de datos (millones de líneas cada uno).
Antes de comenzar a procesar me gustaría contar el número de líneas en el archivo, así puedo indicar qué tan avanzado está el proceso.
Debido al tamaño de los archivos, no sería práctico leer todo el archivo en la memoria, solo para contar cuántas líneas hay. ¿Alguien tiene una buena sugerencia sobre cómo hacer esto?
Con los archivos de texto de estilo UNIX, es muy simple
f = File.new("/path/to/whatever")
num_newlines = 0
while (c = f.getc) != nil
num_newlines += 1 if c == "/n"
end
Eso es. Para los archivos de texto de MS Windows, tendrá que buscar una secuencia de "/ r / n" en lugar de simplemente "/ n", pero eso no es mucho más difícil. Para los archivos de texto Mac OS Classic (a diferencia de Mac OS X), debe buscar "/ r" en lugar de "/ n".
Entonces, sí, esto se ve como C. ¿Y qué? C es increíble y Ruby es increíble porque cuando una respuesta C es más fácil, es lo que puedes esperar que sea tu código Ruby. Afortunadamente, tu dain no ha sido dañado por Java.
Por cierto, por favor, no considere ninguna de las respuestas anteriores que usan el método IO#read
o IO#readlines
llamando a su vez un método String sobre lo que se ha leído. Dijiste que no querías leer todo el archivo en la memoria y eso es exactamente lo que hacen. Esta es la razón por la que Donald Knuth recomienda a las personas que entiendan cómo programar más cerca del hardware, ya que si no lo hacen terminarán escribiendo un "código extraño". Obviamente, no desea codificar cerca del hardware cuando no sea necesario, pero debería ser de sentido común. Sin embargo, debes aprender a reconocer las instancias que tienes que acercarte a los tornillos y tuercas como este.
Y no intente obtener más "orientación a objetos" de lo que exige la situación. Esa es una trampa vergonzosa para los novatos que quieren parecer más sofisticados de lo que realmente son. Siempre debe alegrarse por los momentos en que la respuesta es realmente simple, y no sentirse decepcionado cuando no hay complejidad para darle la oportunidad de escribir un código "impresionante". Sin embargo, si quiere parecer algo "orientado a objetos" y no le importa leer una línea completa en la memoria a la vez (es decir, sabe que las líneas son lo suficientemente cortas), puede hacer esto
f = File.new("/path/to/whatever")
num_newlines = 0
f.each_line do
num_newlines += 1
end
Esto sería un buen compromiso, pero solo si las líneas no son demasiado largas, en cuyo caso podría incluso ejecutarse más rápido que mi primera solución.
Igual que la respuesta de DJ, pero dando el código real de Ruby:
count = %x{wc -l file_path}.split[0].to_i
La primera parte
wc -l file_path
Te dio
num_lines file_path
La split
y to_i
ponen eso en un número.
Leyendo el archivo una línea a la vez:
count = File.foreach(filename).inject(0) {|c, line| c+1}
o el Perl-ish
File.foreach(filename) {}
count = $.
o
count = 0
File.open(filename) {|f| count = f.read.count("/n")}
Será más lento que
count = %x{wc -l #{filename}}.split.first.to_i
Los resultados de la prueba para más de 135k líneas se muestran a continuación. Este es mi código de referencia.
file_name = ''100m.csv''
Benchmark.bm do |x|
x.report { File.new(file_name).readlines.size }
x.report { `wc -l "#{file_name}"`.strip.split('' '')[0].to_i }
x.report { File.read(file_name).scan(//n/).count }
end
resultado es
user system total real
0.100000 0.040000 0.140000 ( 0.143636)
0.000000 0.000000 0.090000 ( 0.093293)
0.380000 0.060000 0.440000 ( 0.464925)
El código wc -l
tiene un problema. Si solo hay una línea en el archivo y el último carácter no termina con /n
, entonces el recuento es cero.
Entonces, recomiendo llamar a wc cuando cuentes más de una línea.
No importa qué idioma estés usando, vas a tener que leer todo el archivo si las líneas son de longitud variable. Esto se debe a que las nuevas líneas podrían estar en cualquier parte y no hay forma de saber sin leer el archivo (suponiendo que no esté almacenado en la memoria caché, lo que en general no lo es).
Si quiere indicar progreso, tiene dos opciones realistas. Puede extrapolar el progreso en función de la longitud de línea supuesta:
assumed lines in file = size of file / assumed line size
progress = lines processed / assumed lines in file * 100%
ya que sabes el tamaño del archivo. Alternativamente, puedes medir el progreso como:
progress = bytes processed / size of file * 100%
Esto debería ser suficiente.
Por razones que no entiendo del todo, escanear el archivo buscando líneas nuevas usando File
parece ser mucho más rápido que hacer CSV#readlines.count
.
El siguiente punto de referencia utilizó un archivo CSV con 1,045,574 líneas de datos y 4 columnas:
user system total real
0.639000 0.047000 0.686000 ( 0.682000)
17.067000 0.171000 17.238000 ( 17.221173)
El código para el índice de referencia está a continuación:
require ''benchmark''
require ''csv''
file = "1-25-2013 DATA.csv"
Benchmark.bm do |x|
x.report { File.read(file).scan(//n/).count }
x.report { CSV.open(file, "r").readlines.count }
end
Como puede ver, escanear el archivo para nuevas líneas es un orden de magnitud más rápido.
Puede leer la última línea solamente y ver su número:
f = File.new(''huge-file'')
f.readlines[-1]
count = f.lineno
Si el archivo es un archivo CSV, la longitud de los registros debe ser bastante uniforme si el contenido del archivo es numérico. ¿No tendría sentido dividir el tamaño del archivo por la longitud del registro o una media de los primeros 100 registros?
Tengo este delineador.
puts File.foreach(''myfile.txt'').count
Usar foreach
sin inject
es aproximadamente un 3% más rápido que con la inject
. Ambos son mucho más rápidos (más de 100x en mi experiencia) que el uso de getc
.
El uso de foreach
sin inject
también se puede simplificar levemente (en relación con el fragmento dado en otro lugar de este hilo) de la siguiente manera:
count = 0; File.foreach(path) { count+=1}
puts "count: #{count}"
usando ruby:
file=File.open("path-to-file","r")
file.readlines.size
39 milisegundos más rápido que wc -l en un archivo de 325.477 líneas
wc -l
en Ruby con menos memoria, la manera perezosa:
(ARGV.length == 0 ?
[["", STDIN]] :
ARGV.lazy.map { |file_name|
[file_name, File.open(file_name)]
})
.map { |file_name, file|
"%8d %s/n" % [*file
.each_line
.lazy
.map { |line| 1 }
.reduce(:+), file_name]
}
.each(&:display)
como fue mostrado originalmente por Shugo Maeda .
Ejemplo:
$ curl -s -o wc.rb -L https://git.io/vVrQi
$ chmod u+x wc.rb
$ ./wc.rb huge_data_file.csv
43217291 huge_data_file.csv
Resumen de las soluciones publicadas
require ''benchmark''
require ''csv''
filename = "name.csv"
Benchmark.bm do |x|
x.report { `wc -l < #{filename}`.to_i }
x.report { File.open(filename).inject(0) { |c, line| c + 1 } }
x.report { File.foreach(filename).inject(0) {|c, line| c+1} }
x.report { File.read(filename).scan(//n/).count }
x.report { CSV.open(filename, "r").readlines.count }
end
Archivo con 807802 líneas:
user system total real
0.000000 0.000000 0.010000 ( 0.030606)
0.370000 0.050000 0.420000 ( 0.412472)
0.360000 0.010000 0.370000 ( 0.374642)
0.290000 0.020000 0.310000 ( 0.315488)
3.190000 0.060000 3.250000 ( 3.245171)
Si se encuentra en un entorno Unix, puede dejar que wc -l
haga el trabajo.
No cargará todo el archivo en la memoria; dado que está optimizado para la transmisión de archivos y contar palabra / línea, el rendimiento es lo suficientemente bueno en lugar de transmitir el archivo usted mismo en Ruby.
SSCCE:
filename = ''a_file/somewhere.txt''
line_count = `wc -l "#{filename}"`.strip.split('' '')[0].to_i
p line_count
O si desea una colección de archivos pasados en la línea de comando:
wc_output = `wc -l "#{ARGV.join(''" "'')}"`
line_count = wc_output.match(/^ *([0-9]+) +total$/).captures[0].to_i
p line_count