Come cercare un pattern nel testo del file e sostituirlo con un dato valore


117

Sto cercando uno script per cercare un file (o un elenco di file) per un pattern e, se trovato, sostituire quel pattern con un dato valore.

Pensieri?


1
Nelle risposte seguenti, tieni presente che qualsiasi consiglio da utilizzare File.readdeve essere temperato con le informazioni in stackoverflow.com/a/25189286/128421 per il motivo per cui lo slurping di file di grandi dimensioni è dannoso. Inoltre, invece di File.open(filename, "w") { |file| file << content }utilizzare variazioni File.write(filename, content).
Tin Man

Risposte:


190

Dichiarazione di non responsabilità: questo approccio è un'illustrazione ingenua delle capacità di Ruby e non una soluzione di livello di produzione per la sostituzione di stringhe nei file. È soggetto a vari scenari di errore, come la perdita di dati in caso di crash, interruzione o disco pieno. Questo codice non è adatto a nulla oltre a un rapido script una tantum in cui viene eseguito il backup di tutti i dati. Per questo motivo, NON copiare questo codice nei tuoi programmi.

Ecco un modo breve e veloce per farlo.

file_names = ['foo.txt', 'bar.txt']

file_names.each do |file_name|
  text = File.read(file_name)
  new_contents = text.gsub(/search_regexp/, "replacement string")

  # To merely print the contents of the file, use:
  puts new_contents

  # To write changes to the file, use:
  File.open(file_name, "w") {|file| file.puts new_contents }
end

Mette riscrive la modifica nel file? Ho pensato che avrebbe semplicemente stampato il contenuto sulla console.
Dane O'Connor

Sì, stampa il contenuto sulla console.
sepp2k

7
Sì, non ero sicuro che fosse quello che volevi. Per scrivere usa File.open (nome_file, "w") {| file | file.puts output_of_gsub}
Max Chernyak

7
Ho dovuto usare file.write: File.open (file_name, "w") {| file | file.write (text)}
austen

3
Per scrivere il file, sostituire la riga put 'conFile.write(file_name, text.gsub(/regexp/, "replace")
tight

106

In realtà, Ruby ha una funzione di modifica sul posto. Come Perl, puoi dire

ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt

Questo applicherà il codice tra virgolette a tutti i file nella directory corrente i cui nomi terminano con ".txt". Verranno create copie di backup dei file modificati con l'estensione ".bak" ("foobar.txt.bak" credo).

NOTA: questo non sembra funzionare per le ricerche su più righe. Per quelli, devi farlo in un altro modo meno carino, con uno script wrapper attorno alla regex.


1
Che diavolo è pi.bak? Senza questo, ottengo un errore. -e: 1: in <main>': undefined method gsub 'per main: Object (NoMethodError)
Ninad

15
@NinadPachpute -imodifiche in atto. .bakè l'estensione utilizzata per un file di backup (opzionale). -pè qualcosa di simile while gets; <script>; puts $_; end. ( $_è l'ultima riga letta, ma puoi assegnarla per qualcosa di simile echo aa | ruby -p -e '$_.upcase!'.)
Lri

1
Questa è una risposta migliore rispetto alla risposta accettata, IMHO, se stai cercando di modificare il file.
Colin K

6
Come posso usarlo all'interno di uno script Ruby ??
Saurabh

1
Ci sono molti modi in cui questo può andare storto, quindi testalo accuratamente prima di tentarlo su un file critico.
Tin Man

49

Tieni presente che, quando lo fai, lo spazio del file system potrebbe essere esaurito e potresti creare un file di lunghezza zero. Questo è catastrofico se stai facendo qualcosa come scrivere file / etc / passwd come parte della gestione della configurazione del sistema.

Nota che la modifica sul posto del file come nella risposta accettata troncerà sempre il file e scriverà il nuovo file in sequenza. Ci sarà sempre una condizione di competizione in cui i lettori simultanei vedranno un file troncato. Se il processo viene interrotto per qualsiasi motivo (ctrl-c, OOM killer, crash del sistema, interruzione di corrente, ecc.) Durante la scrittura, anche il file troncato verrà lasciato, il che può essere catastrofico. Questo è il tipo di scenario di perdita di dati che gli sviluppatori DEVONO considerare perché accadrà. Per questo motivo, penso che la risposta accettata molto probabilmente non dovrebbe essere la risposta accettata. Come minimo, scrivi in ​​un file temporaneo e sposta / rinomina il file in posizione come la soluzione "semplice" alla fine di questa risposta.

È necessario utilizzare un algoritmo che:

  1. Legge il vecchio file e scrive nel nuovo file. (È necessario fare attenzione a risucchiare interi file in memoria).

  2. Chiude esplicitamente il nuovo file temporaneo, che è il punto in cui potresti generare un'eccezione perché i buffer dei file non possono essere scritti su disco perché non c'è spazio. (Prendi questo e ripulisci il file temporaneo, se lo desideri, ma a questo punto devi rilanciare qualcosa o fallire abbastanza difficile.

  3. Corregge le autorizzazioni e le modalità del file sul nuovo file.

  4. Rinomina il nuovo file e lo rilascia in posizione.

Con i filesystem ext3 si ha la garanzia che i metadati scritti per spostare il file in posizione non verranno riorganizzati dal filesystem e scritti prima che i buffer di dati per il nuovo file siano scritti, quindi questo dovrebbe riuscire o fallire. Anche il filesystem ext4 è stato aggiornato per supportare questo tipo di comportamento. Se sei molto paranoico dovresti chiamare la chiamata di fdatasync()sistema come passo 3.5 prima di spostare il file in posizione.

Indipendentemente dalla lingua, questa è la migliore pratica. Nelle lingue in cui la chiamata close()non genera un'eccezione (Perl o C), è necessario verificare esplicitamente la restituzione di close()e lanciare un'eccezione se fallisce.

Il suggerimento sopra di slurp semplicemente il file in memoria, manipolarlo e scriverlo sul file sarà garantito per produrre file di lunghezza zero su un filesystem completo. È necessario utilizzare sempreFileUtils.mv per spostare in posizione un file temporaneo completamente scritto.

Un'ultima considerazione è il posizionamento del file temporaneo. Se apri un file in / tmp, devi considerare alcuni problemi:

  • Se / tmp è montato su un file system diverso, è possibile eseguire / tmp senza spazio prima di aver scritto il file che sarebbe altrimenti distribuibile nella destinazione del vecchio file.

  • Probabilmente ancora più importante, quando provi a inserire mvil file su un dispositivo montato, verrai convertito in modo trasparente in cpcomportamento. Il vecchio file verrà aperto, il vecchio inode dei file verrà conservato e riaperto e il contenuto del file verrà copiato. Molto probabilmente non è ciò che desideri e potresti incappare in errori di "file di testo occupato" se provi a modificare il contenuto di un file in esecuzione. Questo vanifica anche lo scopo dell'uso dei mvcomandi del filesystem e potresti eseguire il filesystem di destinazione senza spazio con solo un file parzialmente scritto.

    Anche questo non ha nulla a che fare con l'implementazione di Ruby. Il sistema mve i cpcomandi si comportano in modo simile.

Ciò che è più preferibile è aprire un Tempfile nella stessa directory del vecchio file. Ciò garantisce che non ci saranno problemi di spostamento tra dispositivi. Lo mvstesso non dovrebbe mai fallire e dovresti sempre ottenere un file completo e non troncato. Eventuali errori, come dispositivo esaurito, errori di autorizzazione, ecc., Dovrebbero essere riscontrati durante la scrittura del Tempfile.

Gli unici svantaggi dell'approccio alla creazione del Tempfile nella directory di destinazione sono:

  • A volte potresti non essere in grado di aprire un Tempfile lì, come se stai cercando di "modificare" un file in / proc per esempio. Per questo motivo potresti voler tornare indietro e provare / tmp se l'apertura del file nella directory di destinazione non riesce.
  • È necessario disporre di spazio sufficiente sulla partizione di destinazione per contenere sia il vecchio file completo che il nuovo file. Tuttavia, se non hai spazio sufficiente per contenere entrambe le copie, probabilmente sei a corto di spazio su disco e il rischio effettivo di scrivere un file troncato è molto più alto, quindi direi che questo è un compromesso molto scarso al di fuori di alcuni estremamente ristretti (e bene -monitorato) casi limite.

Ecco un po 'di codice che implementa l'algoritmo completo (il codice di Windows non è stato testato e non è finito):

#!/usr/bin/env ruby

require 'tempfile'

def file_edit(filename, regexp, replacement)
  tempdir = File.dirname(filename)
  tempprefix = File.basename(filename)
  tempprefix.prepend('.') unless RUBY_PLATFORM =~ /mswin|mingw|windows/
  tempfile =
    begin
      Tempfile.new(tempprefix, tempdir)
    rescue
      Tempfile.new(tempprefix)
    end
  File.open(filename).each do |line|
    tempfile.puts line.gsub(regexp, replacement)
  end
  tempfile.fdatasync unless RUBY_PLATFORM =~ /mswin|mingw|windows/
  tempfile.close
  unless RUBY_PLATFORM =~ /mswin|mingw|windows/
    stat = File.stat(filename)
    FileUtils.chown stat.uid, stat.gid, tempfile.path
    FileUtils.chmod stat.mode, tempfile.path
  else
    # FIXME: apply perms on windows
  end
  FileUtils.mv tempfile.path, filename
end

file_edit('/tmp/foo', /foo/, "baz")

Ed ecco una versione leggermente più stretta che non si preoccupa di ogni possibile caso limite (se sei su Unix e non ti interessa scrivere su / proc):

#!/usr/bin/env ruby

require 'tempfile'

def file_edit(filename, regexp, replacement)
  Tempfile.open(".#{File.basename(filename)}", File.dirname(filename)) do |tempfile|
    File.open(filename).each do |line|
      tempfile.puts line.gsub(regexp, replacement)
    end
    tempfile.fdatasync
    tempfile.close
    stat = File.stat(filename)
    FileUtils.chown stat.uid, stat.gid, tempfile.path
    FileUtils.chmod stat.mode, tempfile.path
    FileUtils.mv tempfile.path, filename
  end
end

file_edit('/tmp/foo', /foo/, "baz")

Il caso d'uso davvero semplice, per quando non ti interessano le autorizzazioni del file system (o non stai eseguendo come root, o stai eseguendo come root e il file è di proprietà di root):

#!/usr/bin/env ruby

require 'tempfile'

def file_edit(filename, regexp, replacement)
  Tempfile.open(".#{File.basename(filename)}", File.dirname(filename)) do |tempfile|
    File.open(filename).each do |line|
      tempfile.puts line.gsub(regexp, replacement)
    end
    tempfile.close
    FileUtils.mv tempfile.path, filename
  end
end

file_edit('/tmp/foo', /foo/, "baz")

TL; DR : dovrebbe essere usato al posto della risposta accettata come minimo, in tutti i casi, al fine di garantire che l'aggiornamento sia atomico e che i lettori simultanei non vedano i file troncati. Come accennato in precedenza, creare il file Temp nella stessa directory del file modificato è importante qui per evitare che le operazioni mv cross-device vengano tradotte in operazioni cp se / tmp è montato su un dispositivo diverso. Chiamare fdatasync è un ulteriore livello di paranoia, ma incorrerà in un calo delle prestazioni, quindi l'ho omesso da questo esempio poiché non è comunemente praticato.


Invece di aprire un file temporaneo nella directory in cui ti trovi, ne creerà automaticamente uno nella directory dei dati dell'app (su Windows comunque) e da loro puoi fare un file.unlink per eliminarlo ..
13aal

3
Ho davvero apprezzato il pensiero extra che è stato messo in questo. Come principiante, è molto interessante vedere i modelli di pensiero di sviluppatori esperti che non possono solo rispondere alla domanda originale, ma anche commentare il contesto più ampio di ciò che la domanda originale significa effettivamente.
ramijames

La programmazione non consiste solo nel risolvere il problema immediato, ma anche nel pensare in anticipo per evitare altri problemi in agguato. Niente irrita uno sviluppatore senior più che incontrare codice che ha dipinto l'algoritmo in un angolo, costringendo un goffo maldestro, quando un piccolo aggiustamento in precedenza avrebbe portato a un bel flusso. Spesso possono essere necessarie ore o giorni di analisi per comprendere l'obiettivo, quindi poche righe sostituiscono una pagina di vecchio codice. A volte è come una partita a scacchi contro i dati e il sistema.
Tin Man

11

Non c'è davvero un modo per modificare i file sul posto. Quello che di solito fai quando puoi farla franca (cioè se i file non sono troppo grandi) è leggere il file in memory ( File.read), eseguire le sostituzioni sulla stringa di lettura ( String#gsub) e quindi riscrivere la stringa modificata nel file ( File.open, File#write).

Se i file sono abbastanza grandi da non essere fattibile, quello che devi fare è leggere il file in blocchi (se il modello che vuoi sostituire non si estende su più righe, un pezzo di solito significa una riga - puoi usare File.foreachper leggere un file riga per riga), e per ogni blocco eseguire la sostituzione su di esso e aggiungerlo a un file temporaneo. Quando hai finito di iterare sul file sorgente, lo chiudi e FileUtils.mvlo sovrascrivi con il file temporaneo.


1
Mi piace l'approccio dello streaming. Ci occupiamo di file di grandi dimensioni contemporaneamente, quindi di solito non abbiamo lo spazio nella RAM per leggere l'intero file
Shane

" Perché" slurping "un file non è una buona pratica? " Potrebbe essere utile leggere in relazione a questo.
Tin Man

9

Un altro approccio consiste nell'usare la modifica sul posto all'interno di Ruby (non dalla riga di comando):

#!/usr/bin/ruby

def inplace_edit(file, bak, &block)
    old_stdout = $stdout
    argf = ARGF.clone

    argf.argv.replace [file]
    argf.inplace_mode = bak
    argf.each_line do |line|
        yield line
    end
    argf.close

    $stdout = old_stdout
end

inplace_edit 'test.txt', '.bak' do |line|
    line = line.gsub(/search1/,"replace1")
    line = line.gsub(/search2/,"replace2")
    print line unless line.match(/something/)
end

Se non desideri creare un backup, '.bak'passa a ''.


1
Sarebbe meglio che provare a slurp ( read) il file. È scalabile e dovrebbe essere molto veloce.
Tin Man

C'è un bug da qualche parte che causa il fallimento di Ruby 2.3.0p0 su Windows con autorizzazione negata se ci sono diversi blocchi consecutivi inplace_edit che lavorano sullo stesso file. Per riprodurre i test di ricerca1 e ricerca2 divisi in 2 blocchi. Non si chiude completamente?
mlt

Mi aspetto problemi con più modifiche di un file di testo che si verificano contemporaneamente. Se non altro potresti ottenere un file di testo gravemente alterato.
Tin Man

7

Questo funziona per me:

filename = "foo"
text = File.read(filename) 
content = text.gsub(/search_regexp/, "replacestring")
File.open(filename, "w") { |file| file << content }

6

Ecco una soluzione per trovare / sostituire in tutti i file di una determinata directory. Fondamentalmente ho preso la risposta fornita da sepp2k e l'ho ampliata.

# First set the files to search/replace in
files = Dir.glob("/PATH/*")

# Then set the variables for find/replace
@original_string_or_regex = /REGEX/
@replacement_string = "STRING"

files.each do |file_name|
  text = File.read(file_name)
  replace = text.gsub!(@original_string_or_regex, @replacement_string)
  File.open(file_name, "w") { |file| file.puts replace }
end

4
require 'trollop'

opts = Trollop::options do
  opt :output, "Output file", :type => String
  opt :input, "Input file", :type => String
  opt :ss, "String to search", :type => String
  opt :rs, "String to replace", :type => String
end

text = File.read(opts.input)
text.gsub!(opts.ss, opts.rs)
File.open(opts.output, 'w') { |f| f.write(text) }

2
È più utile fornire una spiegazione del motivo per cui questa è la soluzione preferita e spiegare come funziona. Vogliamo educare, non solo fornire codice.
Tin Man

trollop è stato rinominato optimist github.com/manageiq/optimist . Inoltre è solo un parser di opzioni CLI non realmente richiesto per rispondere alla domanda.
noraj

1

Se è necessario eseguire sostituzioni oltre i confini delle linee, l'utilizzo ruby -pi -enon funzionerà perché pelabora una riga alla volta. Invece, consiglio quanto segue, anche se potrebbe non riuscire con un file multi-GB:

ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))"

Il sta cercando uno spazio bianco (potenzialmente includendo nuove righe) seguito da una citazione, nel qual caso si sbarazza degli spazi bianchi. Il %q(')è solo un modo elegante per citare il carattere preventivo.


1

Ecco un'alternativa a quella di jim, questa volta in una sceneggiatura

ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))}

Salvalo in uno script, ad es. Replace.rb

Inizi dalla riga di comando con

replace.rb *.txt <string_to_replace> <replacement>

* .txt può essere sostituito con un'altra selezione o con alcuni nomi di file o percorsi

suddiviso in modo da poter spiegare cosa sta succedendo ma ancora eseguibile

# ARGV is an array of the arguments passed to the script.
ARGV[0..-3].each do |f| # enumerate the arguments of this script from the first to the last (-1) minus 2
  File.write(f,  # open the argument (= filename) for writing
    File.read(f) # open the argument (= filename) for reading
    .gsub(ARGV[-2],ARGV[-1])) # and replace all occurances of the beforelast with the last argument (string)
end

EDIT: se vuoi usare un'espressione regolare usa invece questa Ovviamente, questo è solo per la gestione di file di testo relativamente piccoli, nessun mostro Gigabyte

ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}

Questo codice non funzionerà. Suggerirei di testarlo prima di pubblicare, quindi copiare e incollare il codice funzionante.
Tin Man

@theTinMan I test sempre prima della pubblicazione, se possibile. L'ho provato e funziona, sia la versione breve che quella commentata. Perché pensi che non lo sarebbe?
peter

se intendi usare un'espressione regolare vedi la mia modifica, anch'essa testata:>)
peter
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.