Leggi, modifica e scrivi un file di testo in linea usando Ruby

C’è un buon modo per leggere, modificare e scrivere i file sul posto in Ruby?

Nella mia ricerca online ho trovato cose che suggeriscono di leggere tutto in un array, modificare detto array, quindi scrivere tutto. Sento che dovrebbe esserci una soluzione migliore, specialmente se ho a che fare con un file molto grande.

Qualcosa di simile a:

myfile = File.open("path/to/file.txt", "r+") myfile.each do |line| myfile.replace_puts('blah') if line =~ /myregex/ end myfile.close 

Dove replace_puts scriverà sulla riga corrente, anziché su (sopra) scrivendo la riga successiva come al momento, perché il puntatore si trova alla fine della riga (dopo il separatore).

Quindi ogni riga che corrisponde a /myregex/ verrà sostituita con ‘blah’. Ovviamente quello che ho in mente è un po ‘più complicato di quello, per quanto riguarda l’elaborazione, e lo farei in una riga, ma l’idea è la stessa: voglio leggere un file riga per riga e modificare certe righe, e scrivere quando ho finito.

Forse c’è un modo per dire “riavvolgi subito dopo l’ultimo separatore”? O un modo di usare each_with_index e scrivere tramite un numero di indice di riga? Non sono riuscito a trovare nulla del genere, però.

La soluzione migliore che ho finora è quella di leggere le cose in senso lineare, scriverle su un nuovo file (temporaneo) line-wise (possibilmente modificato), quindi sovrascrivere il vecchio file con il nuovo file temp ed eliminare. Ancora una volta, mi sembra che ci dovrebbe essere un modo migliore – non penso che avrei dovuto creare un nuovo file 1gig solo per modificare alcune righe in un file 1GB esistente.

In generale, non c’è modo di apportare modifiche arbitrarie nel mezzo di un file. Non è una mancanza di Ruby. È una limitazione del file system: la maggior parte dei file system rende facile ed efficiente far crescere o ridurre il file alla fine, ma non all’inizio o nel mezzo. Quindi non sarai in grado di riscrivere una linea sul posto a meno che le sue dimensioni rimangano le stesse.

Ci sono due modelli generali per modificare un gruppo di linee. Se il file non è troppo grande, leggi tutto in memoria, modificalo e riscrivilo. Ad esempio, aggiungendo “Kilroy era qui” all’inizio di ogni riga di un file:

 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 

Sebbene semplice, questa tecnica è pericolosa: se il programma viene interrotto durante la scrittura del file, ne perderai una parte o la totalità. Ha anche bisogno di usare la memoria per contenere l’intero file. Se uno di questi è un problema, allora potresti preferire la tecnica successiva.

Come puoi notare, puoi scrivere su un file temporaneo. Al termine, rinomina il file temporaneo in modo che sostituisca il file di input:

 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 

Poiché la rinomina ( FileUtils.mv ) è atomica, il file di input riscritto apparirà tutto in una volta. Se il programma viene interrotto, il file sarà stato riscritto o non lo sarà. Non c’è possibilità che venga parzialmente riscritto.

La clausola di ensure non è strettamente necessaria: il file verrà eliminato quando l’istanza di Tempfile viene raccolta automaticamente. Tuttavia, potrebbe volerci un po ‘di tempo. Il blocco ensure assicura che il tempfile venga ripulito immediatamente, senza dover attendere che sia raccolto dalla spazzatura.

Se si desidera sovrascrivere un file riga per riga, è necessario assicurarsi che la nuova riga abbia la stessa lunghezza della linea originale. Se la nuova riga è più lunga, parte di essa verrà scritta sulla riga successiva. Se la nuova linea è più corta, il resto della vecchia linea rimane dove si trova. La soluzione Tempfile è davvero molto più sicura. Ma se sei disposto a correre un rischio:

 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 

Se la dimensione della linea cambia, questa è una possibilità:

 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 

Nel caso in cui si utilizzi Rails o Facets , o si dipenda in altro modo dall’ActiveSupport di Rails, è ansible utilizzare l’estensione atomic_write su File :

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

Dietro le quinte, questo creerà un file temporaneo che successivamente si sposterà sul percorso desiderato, avendo cura di chiudere il file per te.

Clona ulteriormente i permessi dei file del file esistente o, se non ce n’è uno, della directory corrente.

Puoi scrivere nel mezzo di un file ma devi stare attento a mantenere la lunghezza della stringa che sovrascrivi lo stesso, altrimenti sovrascrivi parte del testo seguente. Do un esempio qui usando File.seek, IO :: SEEK_CUR fornisce la posizione corrente del puntatore del file, alla fine della riga appena letta, il +1 è per il carattere CR alla fine della riga.

 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 

Dopo l’esecuzione, alla fine della sceneggiatura ora hai il seguente, non quello che avevi in ​​mente suppongo.

 aaaxxxxx bcccddd dddeee eee 

Prendendo questo in considerazione, la velocità con questa tecnica è molto migliore rispetto al classico metodo “leggi e scrivi su un nuovo file”. Vedi questi benchmark su un file con dati musicali di 1,7 GB di grandezza. Per l’approccio classico ho usato la tecnica di Wayne. Il benchmark è fatto con il metodo .bmbm in modo che la memorizzazione nella cache del file non giochi un grosso problema. I test sono fatti con MRI Ruby 2.3.0 su Windows 7. Le stringhe sono state effettivamente sostituite, ho controllato entrambi i metodi.

 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 

Che ha dato

 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) 

Quindi la sostituzione del file in_file è stata 4 volte più veloce.