50 votes

Lire, éditer et écrire un fichier texte à la ligne en utilisant Ruby

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

Dans mes recherches en ligne, j'ai trouvé des trucs suggérant de tout lire dans un tableau, de modifier ce tableau, puis d'écrire le tout. J'ai l'impression qu'il devrait y avoir une meilleure solution, surtout si j'ai affaire à un très gros fichier.

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, au lieu de (sur)écrire sur la ligne suivante comme il le fait actuellement parce que le pointeur se trouve à la fin de la ligne (après le séparateur).

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

Peut-être y a-t-il un moyen de dire "rembobinez jusqu'à la fin du dernier séparateur" ? Ou une façon d'utiliser each_with_index et écrire via un numéro d'index de ligne ? Je n'ai rien trouvé de tel.

La meilleure solution que j'ai trouvée jusqu'à présent est de lire les données ligne par ligne, de les écrire dans un nouveau fichier (temporaire) ligne par ligne (éventuellement modifié), puis d'écraser l'ancien fichier avec le nouveau fichier temporaire et de l'effacer. Encore une fois, j'ai l'impression qu'il devrait y avoir un meilleur moyen - je ne pense pas que je devrais avoir à créer un nouveau fichier d'un gigaoctet juste pour éditer quelques lignes dans un fichier existant d'un gigaoctet.

71voto

Wayne Conrad Points 31052

En général, il n'y a aucun moyen de faire des modifications arbitraires au milieu d'un fichier. Ce n'est pas une lacune de Ruby. C'est une limitation du système de fichiers : La plupart des systèmes de fichiers permettent facilement et efficacement d'agrandir ou de réduire le fichier à la fin, mais pas au début ou au milieu. Vous ne pourrez donc pas réécrire une ligne à sa place si sa taille ne change pas.

Il existe deux modèles généraux pour la modification d'un ensemble de lignes. Si le fichier n'est pas trop volumineux, il suffit de le lire entièrement en mémoire, de le modifier et de le réécrire. Par exemple, ajouter "Kilroy était ici" 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 en perdrez une partie ou la totalité. Le programme doit également utiliser de la mémoire pour contenir l'ensemble du fichier. Si l'un ou l'autre de ces problèmes vous préoccupe, préférez la technique suivante.

Vous pouvez, comme vous l'avez noté, écrire dans un fichier temporaire. Lorsque vous avez terminé, renommez le fichier temporaire de manière à ce 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 renommage ( FileUtils.mv ) est atomique, le fichier d'entrée réécrit apparaîtra d'un seul coup. Si le programme est interrompu, soit le fichier aura été réécrit, soit il ne l'aura pas été. Il n'y a aucune possibilité qu'il soit partiellement réécrit.

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

8voto

steenslag Points 29662

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 sera écrite sur la ligne suivante. Si la nouvelle ligne est plus courte, le reste de l'ancienne ligne reste à sa place. La solution du fichier temporaire est vraiment plus sûre. Mais si vous êtes prêt à prendre le risque.. :

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, c'est 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

3voto

rkon Points 1847

Au cas où vous utiliseriez Rails ou Facettes ou si vous dépendez de l'application Rails Soutien actif vous pouvez utiliser la fonction écriture atomique extension à File :

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

En coulisses, il crée un fichier temporaire qu'il déplacera plus tard vers le chemin souhaité, en se chargeant de fermer le fichier pour vous.

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

2voto

peter Points 15430

Vous pouvez écrire au milieu d'un fichier, mais vous devez faire attention à ce que la longueur de la chaîne que vous écrasez reste la même, sinon vous écrasez 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 ce qui suit, pas ce que vous aviez à l'esprit je suppose.

aaaxxxxx
bcccddd
dddeee
eee

En prenant cela en considération, la vitesse en utilisant cette technique est bien meilleure que la méthode classique "lire et écrire dans un nouveau fichier". Voir ces benchmarks sur un fichier contenant des données musicales d'une taille de 1,7 Go. Pour l'approche classique, j'ai utilisé la technique de Wayne. Le benchmark est réalisé avec la méthode .bmbm de sorte que la mise en cache du fichier ne joue pas un rôle très important. Les tests sont effectués avec MRI Ruby 2.3.0 sur Windows 7. Les chaînes ont été effectivement 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 

Ce 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 dans le fichier a donc été 4 fois plus rapide.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X