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
Où 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.
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é.
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
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.
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.