2010-12-09 9 views
44

¿Hay alguna forma de leer, editar y escribir archivos en Ruby?Lea, edite y escriba un archivo de texto en línea con Ruby

En mi búsqueda en línea he encontrado cosas que sugieren leer todo en una matriz, modificar dicha matriz, luego escribir todo. Siento que debería haber una solución mejor, especialmente si estoy tratando con un archivo muy grande.

Algo así como:

myfile = File.open("path/to/file.txt", "r+") 

myfile.each do |line| 
    myfile.replace_puts('blah') if line =~ /myregex/ 
end 

myfile.close 

Dónde replace_puts escribiría sobre la línea actual, en lugar de (sobre) a escribir la siguiente línea como lo hace actualmente debido a que el puntero se encuentra al final de la línea (después del separador)

Por lo tanto, cada línea que coincida con /myregex/ será reemplazada por 'blah'. Obviamente, lo que tengo en mente es un poco más complicado que eso, en cuanto al procesamiento, y lo haría en una línea, pero la idea es la misma: quiero leer un archivo línea por línea y editar ciertas líneas, y escribir cuando termine.

¿Tal vez hay una manera de decir "volver a enrollar justo después del último separador"? O alguna forma de usar each_with_index y escribir a través de un número de índice de línea? Aunque no pude encontrar nada por el estilo.

La mejor solución que tengo hasta ahora es leer cosas en línea, escribirlas en un nuevo archivo (temp) en línea (posiblemente editado), luego sobrescribir el archivo anterior con el nuevo archivo temporal y eliminarlo. De nuevo, creo que debería haber una mejor manera: no creo que deba crear un nuevo archivo 1gig solo para editar algunas líneas en un archivo de 1GB existente.

+0

Considere los resultados si su código para leer y sobreescribir falla a mitad del proceso: corre el riesgo de destruir el archivo. –

+0

Muy bien, como una pregunta de seguimiento: desde la línea de comandos, puede hacer esto: ruby ​​-pe "gsub (/ blah /, 'newstuff')" whatev.txt. Eso hace lo que quiero hacer, pero no quiero hacerlo en la línea de comandos así, quiero ponerlo dentro de algo más grande. ¿Puede alguien decirme, internamente, qué está haciendo ese comando que da la ilusión de editar un archivo, línea por línea? ¿Está escribiendo en un archivo temporal o usando matrices? Porque parece funcionar en archivos bastante grandes con bastante rapidez, más que las sugerencias que aquí se ofrecen. – Hsiu

+0

Esa es una gran pregunta. ¿Podría hacer una nueva pregunta? Eso hace que sea mucho más fácil para los demás verlo y responderlo.Además, si esta pregunta fue respondida a su satisfacción, ¿puede aceptar esa respuesta? ¡Gracias! –

Respuesta

6

Si desea sobrescribir un archivo línea por línea, deberá asegurarse de que la nueva línea tenga la misma longitud que la línea original. Si la nueva línea es más larga, parte de ella se escribirá en la siguiente línea. Si la nueva línea es más corta, el resto de la línea anterior simplemente permanece donde está. La solución de tempfile es realmente mucho más segura. Pero si usted está dispuesto a asumir un riesgo:

File.open('test.txt', 'r+') do |f| 
    old_pos = 0 
    f.each do |line| 
     f.pos = old_pos # this is the 'rewind' 
     f.print line.gsub('2010', '2011') 
     old_pos = f.pos 
    end 
end 

Si el tamaño de la línea cambia, esto es una posibilidad:

File.open('test.txt', 'r+') do |f| 
    out = "" 
    f.each do |line| 
     out << line.gsub(/myregex/, 'blah') 
    end 
    f.pos = 0      
    f.print out 
    f.truncate(f.pos)    
end 
+0

¿La segunda solución es apta para archivos grandes que contienen millones de líneas? ¿No ocupará espacio en la memoria esa operación? – mango

62

En general, no hay manera de hacer cambios arbitrarios en el medio de un archivo. No es una deficiencia de Ruby. Es una limitación del sistema de archivos: la mayoría de los sistemas de archivos hacen que sea fácil y eficiente crecer o reducir el archivo al final, pero no al principio o en el medio. Por lo tanto, no podrá volver a escribir una línea en su lugar a menos que su tamaño se mantenga igual.

Hay dos modelos generales para modificar un grupo de líneas. Si el archivo no es demasiado grande, simplemente léalo todo a la memoria, modifíquelo y vuelva a escribirlo. Por ejemplo, la adición de "Kilroy estuvo aquí" al principio de cada línea de un archivo:

path = '/tmp/foo' 
lines = IO.readlines(path).map do |line| 
    'Kilroy was here ' + line 
end 
File.open(path, 'w') do |file| 
    file.puts lines 
end 

Aunque simple, esta técnica tiene un peligro: si el programa se interrumpe al escribir el archivo, si no se pierden parte o todo eso También necesita usar memoria para contener todo el archivo. Si alguno de estos es un problema, entonces puede preferir la siguiente técnica.

Como puede ver, puede escribir en un archivo temporal.Cuando haya terminado, cambie el nombre del archivo temporal para que reemplace el archivo de entrada:

require 'tempfile' 
require 'fileutils' 

path = '/tmp/foo' 
temp_file = Tempfile.new('foo') 
begin 
    File.open(path, 'r') do |file| 
    file.each_line do |line| 
     temp_file.puts 'Kilroy was here ' + line 
    end 
    end 
    temp_file.close 
    FileUtils.mv(temp_file.path, path) 
ensure 
    temp_file.close 
    temp_file.unlink 
end 

Desde el cambio de nombre (FileUtils.mv) es atómica, el archivo de entrada reescrito saltará a la existencia de una sola vez. Si el programa se interrumpe, el archivo se habrá reescrito o no lo hará. No hay posibilidad de que sea parcialmente reescrito.

La cláusula ensure no es estrictamente necesaria: el archivo se eliminará cuando la instancia de Tempfile sea basura. Sin embargo, eso podría tomar un tiempo. El bloque ensure se asegura de que el archivo temporal se limpia de inmediato, sin tener que esperar a que se recolecte la basura.

+1

+1 Siempre es mejor ser conservador al modificar archivos, especialmente los grandes. –

+0

está a punto de cerrar el archivo temp_file, ¿por qué rebobinarlo? – hihell

+0

@hihell, la edición de BookOfGreg agregó el rebobinado; su observación fue: "FileUtils.mv escribirá un archivo en blanco a menos que se rebobine el archivo temporal. También es una buena práctica asegurarse de que el archivo temporal se cierre y se desvincula después del uso". –

1

Sólo en caso de que esté utilizando los carriles o Facets, o que de otra forma dependen rieles ActiveSupport, se puede utilizar el atomic_write extensión a File:

File.atomic_write('path/file') do |file| 
    file.write('your content') 
end 

Detrás de las escenas, esto creará un archivo temporal que luego se moverá a la ruta deseada, teniendo cuidado de cerrar el archivo por usted.

Además clona los permisos de archivo del archivo existente o, si no hay uno, del directorio actual.

0

Puede escribir en el medio de un archivo pero debe tener cuidado de mantener la longitud de la cadena que sobrescribe la misma, de lo contrario sobrescribirá parte del texto siguiente. Doy un ejemplo aquí usando File.seek, IO :: SEEK_CUR le da la posición actual del puntero al archivo, al final de la línea que acaba de leer, el +1 es para el carácter CR al final de la línea.

look_for  = "bbb" 
replace_with = "xxxxx" 

File.open(DATA, 'r+') do |file| 
    file.each_line do |line| 
    if (line[look_for]) 
     file.seek(-(line.length + 1), IO::SEEK_CUR) 
     file.write line.gsub(look_for, replace_with) 
    end 
    end 
end 
__END__ 
aaabbb 
bbbcccddd 
dddeee 
eee 

Después de ejecutado, al final del script ahora tiene lo siguiente, no es lo que tenía en mente, supongo.

aaaxxxxx 
bcccddd 
dddeee 
eee 

Tomando en consideración que, la velocidad por medio de esta técnica es mucho mejor que el clásico 'leer y escribir en un archivo nuevo' método. Vea estos puntos de referencia en un archivo con datos de música de 1,7 GB de tamaño. Para el enfoque clásico usé la técnica de Wayne. El punto de referencia se realiza con el método .bmbm, por lo que el almacenamiento en caché del archivo no representa un gran problema. Las pruebas se realizan con MRI Ruby 2.3.0 en Windows 7. Las cadenas se reemplazaron de manera efectiva, comprobé ambos métodos.

require 'benchmark' 
require 'tempfile' 
require 'fileutils' 

look_for  = "Melissa Etheridge" 
replace_with = "Malissa Etheridge" 
very_big_file = 'D:\Documents\muziekinfo\all.txt'.gsub('\\','/') 

def replace_with file_path, look_for, replace_with 
    File.open(file_path, 'r+') do |file| 
    file.each_line do |line| 
     if (line[look_for]) 
     file.seek(-(line.length + 1), IO::SEEK_CUR) 
     file.write line.gsub(look_for, replace_with) 
     end 
    end 
    end 
end 

def replace_with_classic path, look_for, replace_with 
    temp_file = Tempfile.new('foo') 
    File.foreach(path) do |line| 
    if (line[look_for]) 
     temp_file.write line.gsub(look_for, replace_with) 
    else 
     temp_file.write line 
    end 
    end 
    temp_file.close 
    FileUtils.mv(temp_file.path, path) 
ensure 
    temp_file.close 
    temp_file.unlink 
end 

Benchmark.bmbm do |x| 
    x.report("adapt   ") { 1.times {replace_with very_big_file, look_for, replace_with}} 
    x.report("restore  ") { 1.times {replace_with very_big_file, replace_with, look_for}} 
    x.report("classic adapt ") { 1.times {replace_with_classic very_big_file, look_for, replace_with}} 
    x.report("classic restore") { 1.times {replace_with_classic very_big_file, replace_with, look_for}} 
end 

Que dio

Rehearsal --------------------------------------------------- 
adapt    6.989000 0.811000 7.800000 ( 7.800598) 
restore   7.192000 0.562000 7.754000 ( 7.774481) 
classic adapt 14.320000 9.438000 23.758000 (32.507433) 
classic restore 14.259000 9.469000 23.728000 (34.128093) 
----------------------------------------- total: 63.040000sec 

         user  system  total  real 
adapt    7.114000 0.718000 7.832000 ( 8.639864) 
restore   6.942000 0.858000 7.800000 ( 8.117839) 
classic adapt 14.430000 9.485000 23.915000 (32.195298) 
classic restore 14.695000 9.360000 24.055000 (33.709054) 

Así que la sustitución in_file era 4 veces más rápido.

Cuestiones relacionadas