2010-12-09 12 views
44

Gibt es eine gute Möglichkeit, Dateien in Ruby zu lesen, zu bearbeiten und zu schreiben?Lesen, bearbeiten und schreiben Sie eine Textdatei zeilenweise mit Ruby

In meiner Online-Suche habe ich Sachen gefunden, die vorschlagen, alles in ein Array zu lesen, das Array zu modifizieren und dann alles zu schreiben. Ich habe das Gefühl, dass es eine bessere Lösung geben sollte, besonders wenn es sich um eine sehr große Datei handelt.

Etwas wie:

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

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

myfile.close 

Wo replace_puts würde die aktuelle Zeile schreiben über, anstatt (über) das Schreiben der nächsten Zeile, wie es gegenwärtig der Fall ist, weil der Zeiger am Ende der Leitung ist (nach dem Trenn).

Also wird dann jede Zeile die mit /myregex/ übereinstimmt mit "blah" ersetzt. Offensichtlich ist das, was ich vorhabe, etwas komplizierter als das, was die Verarbeitung angeht, und würde in einer Zeile erfolgen, aber die Idee ist die gleiche - ich möchte eine Datei Zeile für Zeile lesen und bestimmte Zeilen bearbeiten, und Schreibe auf, wenn ich fertig bin.

Vielleicht gibt es eine Möglichkeit, einfach zu sagen "zurückspulen bis kurz nach dem letzten Trennzeichen"? Oder eine Möglichkeit, each_with_index zu verwenden und über eine Zeilenindexnummer zu schreiben? Ich konnte jedoch nichts dergleichen finden.

Die beste Lösung, die ich bisher habe, ist es, Dinge zeilenweise zu lesen, sie in eine neue (temporäre) Datei auszugeben (möglicherweise bearbeitet), dann die alte Datei mit der neuen temporären Datei zu überschreiben und zu löschen. Nochmals, ich habe das Gefühl, dass es einen besseren Weg geben sollte - ich denke nicht, dass ich eine neue 1gig-Datei erstellen müsste, nur um einige Zeilen in einer bestehenden 1-GB-Datei zu bearbeiten.

+0

Betrachten Sie die Ergebnisse, wenn Ihr Code zu lesen und überschreiben dann während des Prozesses fehlschlagen: Sie laufen Gefahr, die Datei zu zerstören. –

+0

Okay, als Folgefrage: Von der Kommandozeile aus können Sie das tun: ruby ​​-pe "gsub (/ blah /, 'newstuff')" whatev.txt. Das macht, was ich tun möchte, aber ich möchte es nicht auf der Kommandozeile so machen, ich möchte es in etwas Größeres einfügen. Kann mir jemand intern sagen, was dieser Befehl macht, der die Illusion gibt, eine Datei Zeile für Zeile zu bearbeiten? Wird in eine temporäre Datei geschrieben oder werden Arrays verwendet? Weil es ziemlich schnell auf ziemlich großen Dateien zu funktionieren scheint, mehr als die hier angebotenen Vorschläge. – Hsiu

+0

Das ist eine gute Frage. Könnten Sie bitte eine neue Frage stellen? Das macht es viel einfacher für andere, es zu sehen und zu beantworten.Wenn diese Frage zu Ihrer Zufriedenheit beantwortet wurde, können Sie diese Antwort auch akzeptieren? Vielen Dank! –

Antwort

6

Wenn Sie eine Datei zeilenweise überschreiben möchten, müssen Sie sicherstellen, dass die neue Zeile dieselbe Länge wie die ursprüngliche Zeile hat. Wenn die neue Zeile länger ist, wird ein Teil davon über die nächste Zeile geschrieben. Wenn die neue Linie kürzer ist, bleibt der Rest der alten Linie einfach dort, wo sie ist. Die Tempfile-Lösung ist wirklich viel sicherer. Aber wenn Sie bereit sind, ein Risiko einzugehen:

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 

Wenn die Zeilengröße sich ändert, ist dies eine Möglichkeit ist:

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

Ist die 2. Lösung für große Dateien geeignet, die Millionen von Zeilen enthalten? Wird es nicht Speicherplatz für diese Operation im Speicher benötigen? – mango

62

Im Allgemeinen gibt es keine Möglichkeit beliebige Änderungen in der Mitte zu machen einer Datei. Es ist kein Mangel an Ruby. Es ist eine Einschränkung des Dateisystems: Die meisten Dateisysteme machen es einfach und effizient, die Datei am Ende zu vergrößern oder zu verkleinern, aber nicht am Anfang oder in der Mitte. Sie können also keine Zeile neu schreiben, wenn die Größe nicht gleich bleibt.

Es gibt zwei allgemeine Modelle zum Ändern einer Reihe von Zeilen. Wenn die Datei nicht zu groß ist, lesen Sie sie einfach in den Speicher, ändern Sie sie und schreiben Sie sie zurück. Zum Beispiel das Hinzufügen zu Beginn jeder Zeile einer Datei „Kilroy war hier“:

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 

Obwohl einfach, hat diese Technik eine Gefahr: Wenn das Programm unterbrochen wird, während die Datei zu schreiben, werden Sie Teil verlieren oder alles davon. Es muss auch Speicher verwenden, um die gesamte Datei zu halten. Wenn einer dieser Punkte ein Problem ist, dann bevorzugen Sie vielleicht die nächste Technik.

Sie können, wie Sie bemerken, in eine temporäre Datei schreiben.Wenn Sie fertig sind, die temporäre Datei umbenennen, damit es die Eingabedatei ersetzt:

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 

Da die Umbenennungs (FileUtils.mv) atomar ist, wird die neu geschrieben Eingabedatei auf einmal in die Existenz Pop. Wenn das Programm unterbrochen wird, wurde die Datei entweder neu geschrieben oder nicht. Es gibt keine Möglichkeit, dass es teilweise neu geschrieben wird.

Die Klausel ensure ist nicht unbedingt erforderlich: Die Datei wird gelöscht, wenn die Tempfile-Instanz als Garbage Collected erfasst wird. Das könnte jedoch eine Weile dauern. Der Block ensure stellt sicher, dass die temporäre Datei sofort bereinigt wird, ohne auf die Sammlung von Müll warten zu müssen.

+1

+1 Es ist immer besser konservativ zu sein, wenn Dateien verändert werden, besonders große. –

+0

Sie sind dabei, die temp_file zu schließen, warum zurückspulen? – hihell

+0

@hihell, BookOfGregs Bearbeitung fügte den Rücklauf hinzu; Seine Bemerkung lautete: "FileUtils.mv schreibt eine leere Datei, wenn die temporäre Datei nicht zurückgespult wird. Außerdem sollten Sie sicherstellen, dass die temporäre Datei nach der Verwendung geschlossen und nicht mehr verknüpft ist." –

1

Gerade falls Sie verwenden Rails oder Facets, oder Sie sonst hängen von Rails' ActiveSupport, können Sie die atomic_write Erweiterung File verwenden:

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

Hinter den Kulissen, wird dies eine temporäre Datei erstellen die Es wird später auf den gewünschten Pfad verschoben und kümmert sich darum, die Datei für Sie zu schließen.

Es klont weiter die Dateiberechtigungen der vorhandenen Datei oder, wenn es keine gibt, des aktuellen Verzeichnisses.

0

Sie können in der Mitte einer Datei schreiben, aber Sie müssen vorsichtig sein, die Länge der Zeichenfolge zu behalten, die Sie überschreiben, andernfalls überschreiben Sie etwas des folgenden Textes. Ich gebe hier ein Beispiel mit File.seek, IO :: SEEK_CUR gibt die aktuelle Position des Dateizeigers, am Ende der Zeile, die gerade gelesen wird, steht +1 für das CR-Zeichen am Ende der Zeile.

Nach ausgeführt, am Ende des Skripts haben Sie jetzt die folgenden, nicht das, was Sie im Sinn hatte, nehme ich an.

aaaxxxxx 
bcccddd 
dddeee 
eee 

, dass die Einnahme in Betracht, die Geschwindigkeit dieser Technik ist viel besser als die klassischen ‚gelesen und in eine neue Datei schreiben‘ -Methode. Siehe diese Benchmarks für eine Datei mit Musikdaten von 1,7 GB groß. Für den klassischen Ansatz habe ich die Technik von Wayne verwendet. Der Benchmark wird mit der .bmbm-Methode durchgeführt, so dass das Caching der Datei keine große Rolle spielt. Tests werden mit MRI Ruby 2.3.0 unter Windows 7 durchgeführt. Die Strings wurden effektiv ersetzt, ich überprüfte beide Methoden.

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 

Welche

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) 

So ist der in_file Ersatz schneller 4mal war gab.

Verwandte Themen