Lees, bewerk en schrijf een tekstbestand regelgewijs met Ruby

Is er een goede manier om bestanden in Ruby te lezen, te bewerken en te schrijven?

Tijdens mijn online zoektocht heb ik dingen gevonden die suggereren om alles in een array te lezen, de array aan te passen en dan alles weg te schrijven. Ik heb het gevoel dat er een betere oplossing zou moeten zijn, vooral als ik te maken heb met een heel groot bestand.

Zoiets als:

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

Waar replace_putsover de huidige regel zou schrijven, in plaats van de volgende regel te (over)schrijven zoals momenteel het geval is, omdat de aanwijzer zich aan het einde van de regel bevindt (na het scheidingsteken).

p>

Dus dan wordt elke regel die overeenkomt met /myregex/vervangen door ‘blah’. Het is duidelijk dat wat ik in gedachten heb een beetje ingewikkelder is dan dat, voor zover het verwerking betreft, en zou in één regel worden gedaan, maar het idee is hetzelfde – ik wil een bestand regel voor regel lezen en bepaalde regels bewerken, en schrijf op als ik klaar ben.

Misschien is er een manier om gewoon te zeggen “terugspoelen naar net na het laatste scheidingsteken”? Of een manier om each_with_indexte gebruiken en via een regelindexnummer te schrijven? Ik kon echter niets van dien aard vinden.

De beste oplossing die ik tot nu toe heb, is om dingen regelgewijs te lezen, ze regelgewijs naar een nieuw (tijdelijk) bestand te schrijven (mogelijk bewerkt), dan het oude bestand te overschrijven met het nieuwe tijdelijke bestand en te verwijderen. Nogmaals, ik heb het gevoel dat er een betere manier zou moeten zijn – ik denk niet dat ik een nieuw 1gig-bestand zou moeten maken om alleen maar enkele regels in een bestaand 1GB-bestand te bewerken.


Antwoord 1, autoriteit 100%

Over het algemeen is het niet mogelijk om willekeurige bewerkingen uit te voeren in het midden van een bestand. Het is geen gebrek aan Ruby. Het is een beperking van het bestandssysteem: de meeste bestandssystemen maken het gemakkelijk en efficiënt om het bestand aan het einde te laten groeien of verkleinen, maar niet aan het begin of in het midden. U kunt een regel dus niet op zijn plaats herschrijven, tenzij de grootte hetzelfde blijft.

Er zijn twee algemene modellen voor het wijzigen van een aantal regels. Als het bestand niet te groot is, leest u het gewoon in het geheugen, wijzigt u het en schrijft u het weer uit. Bijvoorbeeld door “Kilroy was here” toe te voegen aan het begin van elke regel van een bestand:

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

Hoewel deze techniek eenvoudig is, heeft het een gevaar: als het programma wordt onderbroken tijdens het schrijven van het bestand, verliest u een deel of het geheel. Het moet ook geheugen gebruiken om het hele bestand op te slaan. Als een van beide een probleem is, geeft u misschien de voorkeur aan de volgende techniek.

Je kunt, zoals je hebt opgemerkt, naar een tijdelijk bestand schrijven. Als u klaar bent, hernoemt u het tijdelijke bestand zodat het het invoerbestand vervangt:

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

Aangezien de hernoeming (FileUtils.mv) atomair is, zal het herschreven invoerbestand in één keer verschijnen. Als het programma wordt onderbroken, is het bestand ofwel herschreven, ofwel niet. Het is niet mogelijk dat het gedeeltelijk wordt herschreven.

De ensure-clausule is niet strikt noodzakelijk: het bestand wordt verwijderd wanneer de Tempfile-instantie wordt verzameld. Dat kan echter wel even duren. Het ensure-blok zorgt ervoor dat het tijdelijke bestand meteen wordt opgeschoond, zonder te hoeven wachten tot het wordt verzameld.


Antwoord 2, autoriteit 12%

Als u een bestand regel voor regel wilt overschrijven, moet u ervoor zorgen dat de nieuwe regel dezelfde lengte heeft als de originele regel. Als de nieuwe regel langer is, wordt een deel ervan over de volgende regel geschreven. Als de nieuwe lijn korter is, blijft de rest van de oude lijn gewoon staan.
De tempfile-oplossing is echt veel veiliger. Maar als je bereid bent een risico te nemen:

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

Als de lijngrootte verandert, is dit een mogelijkheid:

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

Antwoord 3, autoriteit 4%

Voor het geval je Rails of Facetsgebruikt, of anderszins afhankelijk bent van Rails’ ActiveSupport, u kunt de atomic_writeextensie naar File:

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

Achter de schermen zal dit een tijdelijk bestand creëren dat later naar het gewenste pad zal worden verplaatst, waarbij het bestand voor u wordt gesloten.

Het kloont verder de bestandsrechten van het bestaande bestand of, als die er niet is, van de huidige map.


Antwoord 4, autoriteit 3%

Je kunt in het midden van een bestand schrijven, maar je moet voorzichtig zijn om de lengte van de string die je overschrijft hetzelfde te houden, anders overschrijf je een deel van de volgende tekst. Ik geef hier een voorbeeld met behulp van File.seek, IO::SEEK_CUR geeft de huidige positie van de bestandsaanwijzer, aan het einde van de regel die zojuist is gelezen, de +1 is voor het CR-teken aan het einde van de regel.

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

Na uitvoering, aan het einde van het script heb je nu het volgende, niet wat je in gedachten had neem ik aan.

aaaxxxxx
bcccddd
dddeee
eee

Dat in overweging nemend, is de snelheid bij het gebruik van deze techniek veel beter dan de klassieke methode ‘lezen en schrijven naar een nieuw bestand’.
Bekijk deze benchmarks op een bestand met muziekgegevens van 1,7 GB groot.
Voor de klassieke aanpak gebruikte ik de techniek van Wayne.
De benchmark wordt gedaan met de .bmbm-methode, zodat het cachen van het bestand niet zo’n groot probleem is. Tests worden gedaan met MRI Ruby 2.3.0 op Windows 7.
De snaren zijn effectief vervangen, ik heb beide methoden gecontroleerd.

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 

Wat gaf

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)

Dus de in_file-vervanging was 4 keer sneller.

Other episodes