So durchsuchen Sie den Dateitext nach einem Muster und ersetzen es durch einen bestimmten Wert

117

Ich suche nach einem Skript, um eine Datei (oder eine Liste von Dateien) nach einem Muster zu durchsuchen und, falls gefunden, dieses Muster durch einen bestimmten Wert zu ersetzen.

Gedanken?

Däne O'Connor
quelle
1
Beachten Sie in den folgenden Antworten, dass alle zu verwendenden Empfehlungen File.readmit den Informationen in stackoverflow.com/a/25189286/128421 gemildert werden müssen, warum das Schlürfen großer Dateien schlecht ist. Auch anstelle von File.open(filename, "w") { |file| file << content }Variationen verwenden File.write(filename, content).
der Blechmann

Antworten:

190

Haftungsausschluss: Dieser Ansatz ist eine naive Darstellung der Funktionen von Ruby und keine produktionsfähige Lösung zum Ersetzen von Zeichenfolgen in Dateien. Es ist anfällig für verschiedene Fehlerszenarien, z. B. Datenverlust im Falle eines Absturzes, einer Unterbrechung oder einer vollen Festplatte. Dieser Code eignet sich nur für ein schnelles einmaliges Skript, in dem alle Daten gesichert werden. Kopieren Sie diesen Code aus diesem Grund NICHT in Ihre Programme.

Hier ist ein kurzer kurzer Weg, um es zu tun.

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
Max Chernyak
quelle
Schreiben Puts die Änderung wieder in die Datei? Ich dachte, das würde nur den Inhalt auf die Konsole drucken.
Dane O'Connor
Ja, der Inhalt wird auf der Konsole gedruckt.
sepp2k
7
Ja, ich war mir nicht sicher, ob du das wolltest. Verwenden Sie zum Schreiben File.open (Dateiname, "w") {| file | file.puts output_of_gsub}
Max Chernyak
7
Ich musste file.write verwenden: File.open (Dateiname, "w") {| file | file.write (text)}
austen
3
Um Write - Datei ersetzen Zeile setzt mitFile.write(file_name, text.gsub(/regexp/, "replace")
fest
106

Tatsächlich verfügt Ruby über eine direkte Bearbeitungsfunktion. Wie Perl kann man sagen

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

Dadurch wird der Code in doppelten Anführungszeichen auf alle Dateien im aktuellen Verzeichnis angewendet, deren Namen mit ".txt" enden. Sicherungskopien der bearbeiteten Dateien werden mit der Erweiterung ".bak" erstellt ("foobar.txt.bak", glaube ich).

HINWEIS: Dies scheint bei mehrzeiligen Suchen nicht zu funktionieren. Für diese muss man es anders machen, mit einem Wrapper-Skript um den regulären Ausdruck.

Jim Kane
quelle
1
Was zum Teufel ist pi.bak? Ohne das bekomme ich einen Fehler. -e: 1: in <main>': undefined method gsub 'für main: Object (NoMethodError)
Ninad
15
@ NinadPachpute wird -ian Ort und Stelle bearbeitet. .bakist die Erweiterung, die für eine Sicherungsdatei verwendet wird (optional). -pist so etwas wie while gets; <script>; puts $_; end. ( $_ist die letzte gelesene Zeile, aber Sie können sie für etwas wie zuweisen echo aa | ruby -p -e '$_.upcase!'.)
Lri
1
Dies ist eine bessere Antwort als die akzeptierte Antwort, IMHO, wenn Sie die Datei ändern möchten.
Colin K
6
Wie kann ich das in einem Ruby-Skript verwenden?
Saurabh
1
Es gibt viele Möglichkeiten, wie dies schief gehen kann. Testen Sie es daher gründlich, bevor Sie es mit einer kritischen Datei versuchen.
der Blechmann
49

Beachten Sie, dass das Dateisystem in diesem Fall möglicherweise nicht genügend Speicherplatz hat und Sie möglicherweise eine Datei mit der Länge Null erstellen. Dies ist katastrophal, wenn Sie im Rahmen des Systemkonfigurationsmanagements beispielsweise / etc / passwd-Dateien schreiben.

Beachten Sie, dass die direkte Dateibearbeitung wie in der akzeptierten Antwort die Datei immer abschneidet und die neue Datei nacheinander ausschreibt. Es wird immer eine Race-Bedingung geben, bei der gleichzeitige Leser eine abgeschnittene Datei sehen. Wenn der Prozess während des Schreibvorgangs aus irgendeinem Grund abgebrochen wird (Strg-C, OOM-Killer, Systemabsturz, Stromausfall usw.), bleibt auch die abgeschnittene Datei übrig, was katastrophal sein kann. Dies ist die Art von Datenverlust-Szenario, die Entwickler berücksichtigen müssen, da dies passieren wird. Aus diesem Grund denke ich, dass die akzeptierte Antwort höchstwahrscheinlich nicht die akzeptierte Antwort sein sollte. Schreiben Sie mindestens in ein Tempfile und verschieben / benennen Sie die Datei wie die "einfache" Lösung am Ende dieser Antwort.

Sie müssen einen Algorithmus verwenden, der:

  1. Liest die alte Datei und schreibt in die neue Datei. (Sie müssen vorsichtig sein, wenn Sie ganze Dateien in den Speicher schlürfen).

  2. Schließt die neue temporäre Datei explizit. Hier können Sie eine Ausnahme auslösen, da die Dateipuffer nicht auf die Festplatte geschrieben werden können, da kein Speicherplatz vorhanden ist. (Fangen Sie dies ab und bereinigen Sie die temporäre Datei, wenn Sie möchten, aber Sie müssen etwas neu werfen oder an dieser Stelle ziemlich hart scheitern.

  3. Behebt die Dateiberechtigungen und -modi für die neue Datei.

  4. Benennt die neue Datei um und legt sie ab.

Mit ext3-Dateisystemen wird garantiert, dass das Schreiben von Metadaten zum Verschieben der Datei nicht vom Dateisystem neu angeordnet und geschrieben wird, bevor die Datenpuffer für die neue Datei geschrieben werden. Dies sollte also entweder erfolgreich sein oder fehlschlagen. Das ext4-Dateisystem wurde ebenfalls gepatcht, um diese Art von Verhalten zu unterstützen. Wenn Sie sehr paranoid sind, sollten Sie den fdatasync()Systemaufruf als Schritt 3.5 aufrufen, bevor Sie die Datei verschieben.

Unabhängig von der Sprache ist dies eine bewährte Methode. In Sprachen, in denen der Aufruf close()keine Ausnahme auslöst (Perl oder C), müssen Sie die Rückgabe von explizit überprüfen close()und eine Ausnahme auslösen, wenn dies fehlschlägt.

Mit dem obigen Vorschlag, die Datei einfach in den Speicher zu schlürfen, zu bearbeiten und in die Datei zu schreiben, werden garantiert Dateien mit der Länge Null auf einem vollständigen Dateisystem erstellt. Sie müssen immer verwenden FileUtils.mv, um eine vollständig geschriebene temporäre Datei an ihren Platz zu verschieben.

Eine letzte Überlegung ist die Platzierung der temporären Datei. Wenn Sie eine Datei in / tmp öffnen, müssen Sie einige Probleme berücksichtigen:

  • Wenn / tmp auf einem anderen Dateisystem bereitgestellt ist, wird / tmp möglicherweise nicht genügend Speicherplatz haben, bevor Sie die Datei geschrieben haben, die andernfalls am Ziel der alten Datei bereitgestellt werden könnte.

  • Wahrscheinlich noch wichtiger: Wenn Sie versuchen, mvdie Datei über einen Geräte-Mount zu übertragen, werden Sie transparent in cpVerhalten konvertiert . Die alte Datei wird geöffnet, der Inode der alten Dateien bleibt erhalten und wird erneut geöffnet, und der Dateiinhalt wird kopiert. Dies ist höchstwahrscheinlich nicht das, was Sie möchten, und es können Fehler "Textdatei beschäftigt" auftreten, wenn Sie versuchen, den Inhalt einer laufenden Datei zu bearbeiten. Dies macht auch den Zweck der Verwendung der Dateisystembefehle mvzunichte und Sie können das Zieldateisystem mit nur einer teilweise geschriebenen Datei aus dem Speicherplatz ausführen.

    Dies hat auch nichts mit Rubys Implementierung zu tun. Das System mvund die cpBefehle verhalten sich ähnlich.

Was besser ist, ist das Öffnen einer Tempfile im selben Verzeichnis wie die alte Datei. Dies stellt sicher, dass keine geräteübergreifenden Verschiebungsprobleme auftreten. Das mvselbst sollte niemals fehlschlagen, und Sie sollten immer eine vollständige und nicht abgeschnittene Datei erhalten. Alle Fehler, wie z. B. nicht genügend Speicherplatz auf dem Gerät, Berechtigungsfehler usw., sollten beim Ausschreiben der Tempfile auftreten.

Die einzigen Nachteile beim Erstellen der Tempfile im Zielverzeichnis sind:

  • Manchmal können Sie dort möglicherweise kein Tempfile öffnen, z. B. wenn Sie beispielsweise versuchen, eine Datei in / proc zu bearbeiten. Aus diesem Grund möchten Sie möglicherweise auf / tmp zurückgreifen, wenn das Öffnen der Datei im Zielverzeichnis fehlschlägt.
  • Auf der Zielpartition muss genügend Speicherplatz vorhanden sein, damit sowohl die vollständige alte als auch die neue Datei gespeichert werden können. Wenn Sie jedoch nicht genügend Speicherplatz haben, um beide Kopien zu speichern, ist der Speicherplatz wahrscheinlich knapp und das tatsächliche Risiko, eine abgeschnittene Datei zu schreiben, ist viel höher. Daher würde ich behaupten, dass dies ein sehr schlechter Kompromiss ist, abgesehen von einigen äußerst engen (und gut) -überwachte) Randfälle.

Hier ist ein Code, der den vollständigen Algorithmus implementiert (Windows-Code ist ungetestet und unvollendet):

#!/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")

Und hier ist eine etwas engere Version, die sich nicht um jeden möglichen Randfall kümmert (wenn Sie unter Unix arbeiten und sich nicht für das Schreiben an / proc interessieren):

#!/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")

Der wirklich einfache Anwendungsfall, wenn Sie sich nicht für Dateisystemberechtigungen interessieren (entweder werden Sie nicht als root ausgeführt oder Sie werden als root ausgeführt und die Datei befindet sich im Root-Besitz):

#!/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 : Dies sollte in jedem Fall mindestens anstelle der akzeptierten Antwort verwendet werden, um sicherzustellen, dass das Update atomar ist und gleichzeitige Leser keine abgeschnittenen Dateien sehen. Wie oben erwähnt, ist es hier wichtig, das Tempfile im selben Verzeichnis wie die bearbeitete Datei zu erstellen, um zu vermeiden, dass geräteübergreifende MV-Vorgänge in CP-Vorgänge übersetzt werden, wenn / tmp auf einem anderen Gerät bereitgestellt wird. Das Aufrufen von fdatasync ist eine zusätzliche Ebene der Paranoia, führt jedoch zu einem Leistungseinbruch. Daher habe ich es in diesem Beispiel weggelassen, da es nicht häufig praktiziert wird.

lamont
quelle
Anstatt eine temporäre Datei in dem Verzeichnis zu öffnen, in dem Sie sich befinden, wird automatisch eine im App-Datenverzeichnis erstellt (unter Windows jedenfalls), und Sie können einen file.unlink verwenden, um sie zu löschen.
13aal
3
Ich habe den zusätzlichen Gedanken, der in diese Sache gesteckt wurde, sehr geschätzt. Als Anfänger ist es sehr interessant, die Denkmuster erfahrener Entwickler zu sehen, die nicht nur die ursprüngliche Frage beantworten, sondern auch den größeren Kontext dessen kommentieren können, was die ursprüngliche Frage tatsächlich bedeutet.
Ramijames
Beim Programmieren geht es nicht nur darum, das unmittelbare Problem zu beheben, sondern auch darum, vorausschauend zu denken, um andere Probleme zu vermeiden, die auf der Lauer liegen. Nichts irritiert einen erfahrenen Entwickler mehr, als auf Code zu stoßen, der den Algorithmus in eine Ecke malt und einen unangenehmen Kludge erzwingt, wenn eine geringfügige Anpassung früher zu einem guten Ablauf geführt hätte. Die Analyse kann oft Stunden oder Tage dauern, um das Ziel zu verstehen, und dann ersetzen einige Zeilen eine Seite mit altem Code. Es ist manchmal wie eine Schachpartie gegen die Daten und das System.
der Blechmann
11

Es gibt nicht wirklich eine Möglichkeit, Dateien direkt zu bearbeiten. Was Sie normalerweise tun, wenn Sie damit durchkommen können (dh wenn die Dateien nicht zu groß sind), ist, dass Sie die Datei in den Speicher lesen ( File.read), Ihre Ersetzungen an der gelesenen Zeichenfolge ( String#gsub) vornehmen und dann die geänderte Zeichenfolge zurück in die Datei schreiben Datei ( File.open, File#write).

Wenn die Dateien groß genug dafür zu sein , nicht machbar sind, was Sie tun müssen, ist die Datei in Blöcken gelesen (wenn das Muster , das Sie ersetzen möchten nicht über mehrere Zeilen erstrecken dann einen Chunk bedeutet in der Regel eine Zeile - können Sie verwenden , File.foreachum Lesen Sie eine Datei Zeile für Zeile), und führen Sie für jeden Block die Ersetzung durch und hängen Sie sie an eine temporäre Datei an. Wenn Sie die Quelldatei durchlaufen haben, schließen Sie sie und FileUtils.mvüberschreiben sie mit der temporären Datei.

sepp2k
quelle
1
Ich mag den Streaming-Ansatz. Wir bearbeiten gleichzeitig große Dateien, sodass wir normalerweise nicht genügend Speicherplatz im RAM haben, um die gesamte Datei zu lesen
Shane
" Warum ist das" Schlürfen "einer Datei keine gute Praxis? " Könnte in diesem Zusammenhang hilfreich sein.
der Blechmann
9

Ein anderer Ansatz ist die Verwendung der Inplace-Bearbeitung in Ruby (nicht über die Befehlszeile):

#!/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

Wenn Sie kein Backup erstellen möchten, wechseln Sie '.bak'zu ''.

DavidG
quelle
1
Dies wäre besser, als zu versuchen, readdie Datei zu schlürfen ( ). Es ist skalierbar und sollte sehr schnell sein.
Der Blechmann
Irgendwo ist ein Fehler aufgetreten, der dazu führte, dass Ruby 2.3.0p0 unter Windows fehlschlug und die Berechtigung verweigert wurde, wenn mehrere aufeinanderfolgende inplace_edit-Blöcke an derselben Datei arbeiten. Zum Reproduzieren von Split Search1- und Search2-Tests in 2 Blöcke. Nicht vollständig schließen?
Mlt
Ich würde Probleme mit mehreren Änderungen einer Textdatei gleichzeitig erwarten. Wenn nichts anderes, könnten Sie eine schlecht verstümmelte Textdatei bekommen.
Der Blechmann
7

Das funktioniert bei mir:

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

Hier ist eine Lösung zum Suchen / Ersetzen in allen Dateien eines bestimmten Verzeichnisses. Grundsätzlich habe ich die Antwort von sepp2k genommen und erweitert.

# 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
Gerber
quelle
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) }
Ninad
quelle
2
Es ist hilfreicher, wenn Sie erklären, warum dies die bevorzugte Lösung ist, und erklären, wie es funktioniert. Wir wollen aufklären, nicht nur Code bereitstellen.
der Blechmann
trollop wurde in optimist github.com/manageiq/optimist umbenannt . Außerdem ist es nur ein CLI-Optionsparser, der zur Beantwortung der Frage nicht wirklich erforderlich ist.
Noraj
1

Wenn Sie Ersetzungen über Zeilengrenzen hinweg durchführen müssen, ruby -pi -efunktioniert die Verwendung nicht, da die pVerarbeitung zeilenweise erfolgt. Stattdessen empfehle ich Folgendes, obwohl es mit einer Datei mit mehreren GB fehlschlagen könnte:

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

Das sucht nach Leerzeichen (möglicherweise einschließlich neuer Zeilen), gefolgt von einem Anführungszeichen. In diesem Fall wird das Leerzeichen entfernt. Das %q(')ist nur eine andere Art , die Anführungszeichen zu zitieren.

Dan Kohn
quelle
1

Hier eine Alternative zum One Liner von Jim, diesmal in einem Skript

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

Speichern Sie es in einem Skript, z. B. replace.rb

Sie beginnen in der Befehlszeile mit

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

* .txt kann durch eine andere Auswahl oder durch einige Dateinamen oder Pfade ersetzt werden

Aufgeschlüsselt, damit ich erklären kann, was passiert, aber immer noch ausführbar ist

# 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

BEARBEITEN: Wenn Sie einen regulären Ausdruck verwenden möchten, verwenden Sie diesen stattdessen. Dies gilt natürlich nur für den Umgang mit relativ kleinen Textdateien, keine Gigabyte-Monster

ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}
Peter
quelle
Dieser Code funktioniert nicht. Ich würde vorschlagen, es vor dem Posten zu testen und dann den Arbeitscode zu kopieren und einzufügen.
der Blechmann
@theTinMan Ich teste immer vor der Veröffentlichung, wenn möglich. Ich habe dies getestet und es funktioniert, sowohl die kurze als auch die kommentierte Version. Warum glaubst du, würde es nicht?
Peter
Wenn Sie meinen, einen regulären Ausdruck zu verwenden, siehe meine Bearbeitung, auch getestet:>)
Peter