web-dev-qa-db-fra.com

Lire, éditer et écrire un fichier texte en ligne avec Ruby

Existe-t-il un bon moyen de lire, d’éditer et d’écrire des fichiers en place dans Ruby?

Dans ma recherche en ligne, j'ai trouvé des éléments suggérant de tout lire dans un tableau, de modifier ledit tableau, puis de tout écrire. Je pense qu'il devrait y avoir une meilleure solution, surtout si je traite un très gros dossier.

Quelque chose comme:

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

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

myfile.close

replace_puts écrirait sur la ligne en cours plutôt que d'écraser la ligne suivante comme c'est le cas actuellement, car le pointeur se trouve à la fin de la ligne (après le séparateur).

Ainsi, chaque ligne correspondant à /myregex/ sera remplacée par "blah". Évidemment, ce que j'ai en tête est un peu plus compliqué que cela, en ce qui concerne le traitement, et serait fait en une seule ligne, mais l'idée est la même - je veux lire un fichier ligne par ligne et éditer certaines lignes, et écris quand j'ai fini.

Peut-être y a-t-il un moyen de simplement dire "revenir en arrière juste après le dernier séparateur"? Ou un moyen d'utiliser each_with_index et d'écrire via un numéro d'index de ligne? Je n'ai rien trouvé de tel, cependant.

La meilleure solution que j’ai jusqu’à présent consiste à lire les choses par ligne, à les écrire dans un nouveau fichier (temp) par ligne (éventuellement édité), puis à remplacer l’ancien fichier par le nouveau fichier temporaire et à le supprimer. Encore une fois, j’ai le sentiment qu’il devrait exister un meilleur moyen - je ne pense pas que je devrais créer un nouveau fichier 1gig simplement pour éditer des lignes dans un fichier existant de 1 Go.

47
Hsiu

En général, il n’existe aucun moyen de procéder à des modifications arbitraires au milieu d’un fichier. Ce n'est pas une carence de Ruby. C'est une limitation du système de fichiers: la plupart des systèmes de fichiers facilitent et optimisent la croissance ou la réduction du fichier à la fin, mais pas au début ni au milieu. Ainsi, vous ne pourrez pas réécrire une ligne sur place à moins que sa taille reste la même.

Il existe deux modèles généraux pour modifier plusieurs lignes. Si le fichier n'est pas trop volumineux, il suffit de tout lire en mémoire, de le modifier et de l'écrire à nouveau. Par exemple, en ajoutant "Kilroy was here" au début de chaque ligne d'un fichier:

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

Bien que simple, cette technique présente un danger: si le programme est interrompu pendant l'écriture du fichier, vous perdrez tout ou partie de celui-ci. Il doit également utiliser la mémoire pour contenir le fichier entier. Si l'une de ces préoccupations vous préoccupe, vous préférerez peut-être la technique suivante.

Comme vous le constatez, vous pouvez écrire dans un fichier temporaire. Une fois terminé, renommez le fichier temporaire afin qu'il remplace le fichier d'entrée:

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

Étant donné que le changement de nom (FileUtils.mv) est atomique, le fichier d’entrée réécrit apparaîtra simultanément. Si le programme est interrompu, le fichier aura été réécrit ou il ne l’aura pas été. Il n'y a aucune possibilité qu'il soit partiellement réécrit.

La clause ensure n'est pas strictement nécessaire: le fichier sera supprimé lorsque l'instance Tempfile est nettoyée. Cependant, cela pourrait prendre un certain temps. Le bloc ensure permet de s’assurer que le fichier temporaire est nettoyé immédiatement, sans avoir à attendre qu’il soit collecté.

67
Wayne Conrad

Si vous souhaitez écraser un fichier ligne par ligne, vous devez vous assurer que la nouvelle ligne a la même longueur que la ligne d'origine. Si la nouvelle ligne est plus longue, une partie de celle-ci sera écrite sur la ligne suivante. Si la nouvelle ligne est plus courte, le reste de l'ancienne ligne reste juste là où il se trouve .. La solution tempfile est vraiment beaucoup plus sûre. Mais si vous êtes prêt à prendre des risques:

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

Si la taille de la ligne change, voici une 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
7
steenslag

Juste au cas où vous utilisez Rails ou Facets , ou que vous dépendiez de Rails ' ActiveSupport , vous pouvez utiliser l’extension atomic_write pour File:

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

En coulisse, cela créera un fichier temporaire qui sera ensuite déplacé vers le chemin souhaité, en prenant soin de le fermer pour vous. 

Il clone en outre les autorisations de fichier du fichier existant ou, s'il n'y en a pas, du répertoire actuel.

1
Kostas Rousis

Vous pouvez écrire au milieu d'un fichier, mais vous devez garder la longueur de la chaîne que vous écrasez de la même façon, sinon vous écraserez une partie du texte suivant. Je donne un exemple ici en utilisant File.seek, IO :: SEEK_CUR donne la position actuelle du pointeur de fichier, à la fin de la ligne qui vient d'être lue, le +1 est pour le caractère CR à la fin de la ligne.

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

Après exécution, à la fin du script, vous avez maintenant les éléments suivants, pas ce que vous aviez en tête, je suppose.

aaaxxxxx
bcccddd
dddeee
eee

Compte tenu de ce qui précède, l'utilisation de cette technique est beaucoup plus rapide que la méthode classique "lire et écrire dans un nouveau fichier" . Visualisez ces points de repère sur un fichier contenant 1,7 Go de données musicales. J'ai utilisé la technique de Wayne ... Le test de référence est effectué à l'aide de la méthode .bmbm, de sorte que la mise en cache du fichier ne joue pas très grave. Les tests sont effectués avec MRI Ruby 2.3.0 sous Windows 7 . Les chaînes ont été remplacées, j'ai vérifié les deux méthodes.

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 

Qui a donné

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)

Le remplacement de in_file était donc 4 fois plus rapide.

0
peter