2010-12-09 85 views
44

Existe-t-il un bon moyen de lire, de modifier et d'écrire des fichiers dans Ruby?Lire, modifier et écrire un fichier texte par ligne à l'aide de Ruby

Dans ma recherche en ligne, j'ai trouvé des trucs suggérant de tout lire dans un tableau, de modifier le tableau, puis de tout écrire. Je pense 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 actuelle, plutôt que (sur) écrire 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 correspondant à /myregex/ sera remplacée par «blah». Évidemment, ce que j'ai en tête est un peu plus compliqué que ça, en ce qui concerne le traitement, et ce serait fait en une 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 dire simplement "revenir en arrière juste après le dernier séparateur"? Ou une certaine façon d'utiliser each_with_index et écrire via un numéro d'index de ligne? Cependant, je n'ai rien trouvé de semblable. La meilleure solution que j'ai jusqu'à maintenant est de lire les choses par lignes, de les écrire dans un nouveau fichier (temporaire) par ligne (éventuellement modifié), puis d'écraser l'ancien fichier avec le nouveau fichier temporaire et de le supprimer. Encore une fois, je pense qu'il devrait y avoir un meilleur moyen - je ne pense pas que je devrais avoir à créer un nouveau fichier 1gig juste pour éditer quelques lignes dans un fichier existant de 1Go.

+0

Considérez les résultats si votre code à lire puis écraser échouait en cours de processus: Vous courez le risque de détruire le fichier. –

+0

Très bien, comme question de suivi: depuis la ligne de commande, vous pouvez faire ceci: ruby ​​-pe "gsub (/ blah /, 'newstuff')" whatev.txt. Cela fait ce que je veux faire, mais je ne veux pas le faire sur la ligne de commande comme ça, je veux le mettre dans quelque chose de plus grand. Quelqu'un peut-il me dire, en interne, ce que fait cette commande qui donne l'illusion d'éditer un fichier, ligne par ligne? Est-ce qu'il écrit dans un fichier temporaire ou utilise des tableaux? Parce qu'il semble fonctionner assez rapidement sur des fichiers assez volumineux, à plus forte raison que les suggestions proposées jusqu'ici. – Hsiu

+0

C'est une excellente question. Pourriez-vous s'il vous plaît en faire une nouvelle question? Cela rend beaucoup plus facile pour les autres de le voir et d'y répondre.En outre, si cette question a été répondue à votre satisfaction, pouvez-vous accepter cette réponse? Merci! –

Répondre

6

Si vous souhaitez remplacer 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 est. La solution Tempfile est vraiment beaucoup plus sûre. Mais si vous êtes prêt à prendre un 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 ne change, cela 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 
+0

La 2ème solution est-elle adaptée aux gros fichiers contenant des millions de lignes? Cela ne prendra-t-il pas de la place pour cette opération? – mango

62

En général, il n'y a pas moyen de faire des modifications arbitraires au milieu d'un fichier. Ce n'est pas une déficience de Ruby. C'est une limitation du système de fichiers: La plupart des systèmes de fichiers facilitent et réduisent le fichier à la fin, mais pas au début ou au milieu. Vous ne pourrez donc pas réécrire une ligne à moins que sa taille ne reste la même.

Il existe deux modèles généraux pour modifier un groupe de lignes. Si le fichier n'est pas trop grand, il suffit de le lire en mémoire, de le modifier et de le réécrire. 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 a un danger: si le programme est interrompu pendant l'écriture du fichier, vous perdrez une partie ou tout. Il doit également utiliser la mémoire pour contenir le fichier entier. Si l'un de ces problèmes est préoccupant, vous préférerez peut-être la technique suivante.

Vous pouvez, comme vous le notez, écrire dans un fichier temporaire.Lorsque vous avez 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 

Depuis le changement de nom (FileUtils.mv) est atomique, le fichier d'entrée réécrite sautera dans l'existence à la fois. Si le programme est interrompu, le fichier aura été réécrit, ou il ne sera pas. Il n'y a aucune possibilité que cela soit partiellement réécrit.

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

+1

+1 Il est toujours préférable d'être prudent lors de la modification de fichiers, en particulier de gros fichiers. –

+0

vous êtes sur le point de fermer le fichier temp_file, pourquoi le rembobiner? – hihell

+0

@hihell, l'édition de BookOfGreg a ajouté le retour; sa remarque était: "FileUtils.mv écrira un fichier vide à moins que le fichier temporaire ne soit rembobiné.La meilleure pratique consiste à s'assurer que le fichier temporaire est fermé et non lié après utilisation." –

1

Juste au cas où vous utilisez Rails ou Facets, ou vous autrement dépendez on Rails' ActiveSupport, vous pouvez utiliser l'extension atomic_write-File:

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

Dans les coulisses, cela va créer un fichier temporaire il se déplacera plus tard vers le chemin désiré, en prenant soin de fermer le fichier pour vous. En outre, il clone les autorisations de fichier du fichier existant ou, s'il n'y en a pas, du répertoire en cours.

0

Vous pouvez écrire au milieu d'un fichier mais vous devez faire attention à ne pas modifier la longueur de la chaîne, 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. Après l'exécution, à la fin du script vous avez maintenant ce qui suit, pas ce que vous aviez en tête je suppose.

aaaxxxxx 
bcccddd 
dddeee 
eee 

Prenant, en contrepartie, la vitesse en utilisant cette technique est beaucoup mieux que la méthode classique »lire et écrire dans un nouveau fichier. Voir ces cas-tests sur un fichier avec des données de musique de 1,7 Go de large. Pour l'approche classique, j'ai utilisé la technique de Wayne. Le benchmark est fait avec la méthode .bmbm afin que la mise en cache du fichier ne joue pas un très gros problème. Les tests sont effectués avec MRI Ruby 2.3.0 sous Windows 7. Les chaînes ont été remplacées efficacement, 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) 

Ainsi, le remplacement de fichier_entree était 4 fois plus rapide.