136 votes

Comment rechercher un motif dans le texte d'un fichier et le remplacer par une valeur donnée ?

Je cherche un script pour rechercher un motif dans un fichier (ou une liste de fichiers) et, s'il le trouve, remplacer ce motif par une valeur donnée.

Qu'en pensez-vous ?

1 votes

Dans les réponses ci-dessous, gardez à l'esprit que toute recommandation visant à utiliser File.read doivent être tempérées par les informations contenues dans stackoverflow.com/a/25189286/128421 pour expliquer pourquoi il est mauvais d'avaler de gros fichiers. En outre, au lieu de File.open(filename, "w") { |file| file << content } utilisation des variations File.write(filename, content) .

209voto

hakunin Points 13171

Avis de non-responsabilité : Cette approche est une illustration naïve des capacités de Ruby, et non une solution de production pour remplacer les chaînes de caractères dans les fichiers. Elle est sujette à divers scénarios d'échec, comme la perte de données en cas de plantage, d'interruption ou de disque plein. Ce code n'est pas adapté à autre chose qu'un script rapide et unique où toutes les données sont sauvegardées. Pour cette raison, ne copiez PAS ce code dans vos programmes.

Voici un moyen rapide et court de le faire.

file_names = ['foo.txt', 'bar.txt']

file_names.each do |file_name|
  text = File.read(file_name)
  new_contents = text.gsub(/search_regexp/, "replacement string")

  # To merely print the contents of the file, use:
  puts new_contents

  # To write changes to the file, use:
  File.open(file_name, "w") {|file| file.puts new_contents }
end

0 votes

Est-ce que puts réécrit la modification dans le fichier ? Je pensais que cela ne ferait qu'imprimer le contenu dans la console.

0 votes

Oui, il imprime le contenu dans la console.

7 votes

Oui, je n'étais pas sûr que c'était ce que vous vouliez. Pour écrire, utilisez File.open(file_name, "w") {|file| file.puts output_of_gsub}

112voto

Jim Kane Points 676

En fait, Ruby dispose d'une fonction d'édition sur place. Comme Perl, vous pouvez dire

ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt

Cela appliquera le code entre guillemets à tous les fichiers du répertoire actuel dont le nom se termine par ".txt". Des copies de sauvegarde des fichiers modifiés seront créées avec une extension ".bak" ("foobar.txt.bak" je pense).

REMARQUE : cela ne semble pas fonctionner pour les recherches multilignes. Pour celles-ci, vous devez le faire de l'autre manière moins jolie, avec un wrapper script autour de la regex.

1 votes

Que diable est pi.bak ? Sans ça, j'ai une erreur. -e:1:in <main>': undefined method gsub' for main:Object (NoMethodError)

16 votes

@NinadPachpute -i les modifications en place. .bak est l'extension utilisée pour un fichier de sauvegarde (facultatif). -p est quelque chose comme while gets; <script>; puts $_; end . ( $_ est la dernière ligne lue, mais vous pouvez l'assigner pour quelque chose comme echo aa | ruby -p -e '$_.upcase!' .)

1 votes

C'est une meilleure réponse que la réponse acceptée, IMHO, si vous cherchez à modifier le fichier.

57voto

lamont Points 971

N'oubliez pas que, lorsque vous faites cela, le système de fichiers peut manquer d'espace et vous risquez de créer un fichier de longueur nulle. Ceci est catastrophique si vous faites quelque chose comme écrire des fichiers /etc/passwd dans le cadre de la gestion de la configuration du système.

Notez que l'édition de fichiers en place, comme dans la réponse acceptée, tronquera toujours le fichier et écrira le nouveau fichier séquentiellement. Il y aura toujours une condition de course où les lecteurs concurrents verront un fichier tronqué. Si le processus est interrompu pour une raison quelconque (ctrl-c, tueur OOM, crash du système, panne de courant, etc.) pendant l'écriture, le fichier tronqué sera également laissé de côté, ce qui peut être catastrophique. C'est le genre de scénario de perte de données que les développeurs DOIVENT envisager car il se produira. Pour cette raison, je pense que la réponse acceptée ne devrait probablement pas être la réponse acceptée. Au minimum, écrivez dans un fichier temporaire et déplacez/renommez le fichier en place comme la solution "simple" à la fin de cette réponse.

Vous devez utiliser un algorithme qui :

  1. Lit l'ancien fichier et écrit dans le nouveau fichier. (Vous devez faire attention à ne pas mettre en mémoire des fichiers entiers).

  2. Ferme explicitement le nouveau fichier temporaire, et c'est là que vous pouvez lancer une exception parce que les tampons du fichier ne peuvent pas être écrits sur le disque parce qu'il n'y a pas d'espace. (Attrapez cela et nettoyez le fichier temporaire si vous le souhaitez, mais vous devez relancer quelque chose ou échouer assez durement à ce stade.

  3. Corrige les permissions et les modes sur le nouveau fichier.

  4. Renomme le nouveau fichier et le dépose en place.

Avec les systèmes de fichiers ext3, vous avez la garantie que les métadonnées écrites pour déplacer le fichier en place ne seront pas réorganisées par le système de fichiers et écrites avant que les tampons de données pour le nouveau fichier soient écrits, donc cela devrait réussir ou échouer. Le système de fichiers ext4 a également été corrigé pour prendre en charge ce type de comportement. Si vous êtes très paranoïaque, vous devriez appeler la fonction fdatasync() comme une étape 3.5 avant de déplacer le fichier en place.

Quelle que soit la langue, il s'agit d'une bonne pratique. Dans les langues où l'appel close() ne lève pas d'exception (Perl ou C), vous devez vérifier explicitement le retour de la fonction close() et lancer une exception si elle échoue.

La suggestion ci-dessus de simplement faire entrer le fichier en mémoire, de le manipuler et de l'écrire dans le fichier sera garantie pour produire des fichiers de longueur nulle sur un système de fichiers complet. Vous devez toujours utiliser FileUtils.mv pour mettre en place un fichier temporaire entièrement écrit.

Une dernière considération est le placement du fichier temporaire. Si vous ouvrez un fichier dans /tmp, vous devez tenir compte de quelques problèmes :

  • Si /tmp est monté sur un système de fichiers différent, vous risquez de manquer d'espace dans /tmp avant d'avoir écrit le fichier qui serait autrement déployable vers la destination de l'ancien fichier.

  • Probablement plus important, quand vous essayez de mv le fichier à travers un support de périphérique, vous serez converti de manière transparente en cp comportement. L'ancien fichier sera ouvert, l'inode de l'ancien fichier sera conservé et rouvert et le contenu du fichier sera copié. Ce n'est probablement pas ce que vous voulez, et vous pouvez rencontrer des erreurs "fichier texte occupé" si vous essayez de modifier le contenu d'un fichier en cours d'exécution. Cela va également à l'encontre de l'objectif de l'utilisation du système de fichiers mv et vous risquez de manquer d'espace sur le système de fichiers de destination avec un fichier partiellement écrit.

    Cela n'a également rien à voir avec l'implémentation de Ruby. Le système mv et cp se comportent de la même manière.

Ce qui est plus préférable, c'est d'ouvrir un fichier temporaire dans le même répertoire que l'ancien fichier. Cela garantit qu'il n'y aura pas de problèmes de déplacement entre les appareils. Le site mv lui-même ne devrait jamais échouer, et vous devriez toujours obtenir un fichier complet et non tronqué. Tout échec, tel que le manque d'espace sur le périphérique, les erreurs de permission, etc., devrait être rencontré pendant l'écriture du fichier temporaire.

Les seuls inconvénients de l'approche consistant à créer le fichier temporaire dans le répertoire de destination sont les suivants :

  • Il peut arriver que vous ne puissiez pas ouvrir un fichier temporaire à cet endroit, par exemple si vous essayez de "modifier" un fichier dans /proc. Pour cette raison, vous pouvez vous replier et essayer /tmp si l'ouverture du fichier dans le répertoire de destination échoue.
  • Vous devez disposer de suffisamment d'espace sur la partition de destination pour contenir à la fois l'ancien fichier complet et le nouveau fichier. Cependant, si vous n'avez pas assez d'espace pour contenir les deux copies, vous êtes probablement à court d'espace disque et le risque réel d'écrire un fichier tronqué est beaucoup plus élevé, donc je dirais que c'est un très mauvais compromis en dehors de quelques cas limites extrêmement étroits (et bien surveillés).

Voici du code qui implémente l'algorithme complet (le code Windows n'est pas testé et n'est pas terminé) :

#!/usr/bin/env ruby

require 'tempfile'

def file_edit(filename, regexp, replacement)
  tempdir = File.dirname(filename)
  tempprefix = File.basename(filename)
  tempprefix.prepend('.') unless RUBY_PLATFORM =~ /mswin|mingw|windows/
  tempfile =
    begin
      Tempfile.new(tempprefix, tempdir)
    rescue
      Tempfile.new(tempprefix)
    end
  File.open(filename).each do |line|
    tempfile.puts line.gsub(regexp, replacement)
  end
  tempfile.fdatasync unless RUBY_PLATFORM =~ /mswin|mingw|windows/
  tempfile.close
  unless RUBY_PLATFORM =~ /mswin|mingw|windows/
    stat = File.stat(filename)
    FileUtils.chown stat.uid, stat.gid, tempfile.path
    FileUtils.chmod stat.mode, tempfile.path
  else
    # FIXME: apply perms on windows
  end
  FileUtils.mv tempfile.path, filename
end

file_edit('/tmp/foo', /foo/, "baz")

Et voici une version un peu plus stricte qui ne s'inquiète pas de tous les cas de figure possibles (si vous êtes sous Unix et que vous ne vous souciez pas d'écrire dans /proc) :

#!/usr/bin/env ruby

require 'tempfile'

def file_edit(filename, regexp, replacement)
  Tempfile.open(".#{File.basename(filename)}", File.dirname(filename)) do |tempfile|
    File.open(filename).each do |line|
      tempfile.puts line.gsub(regexp, replacement)
    end
    tempfile.fdatasync
    tempfile.close
    stat = File.stat(filename)
    FileUtils.chown stat.uid, stat.gid, tempfile.path
    FileUtils.chmod stat.mode, tempfile.path
    FileUtils.mv tempfile.path, filename
  end
end

file_edit('/tmp/foo', /foo/, "baz")

Le cas d'utilisation le plus simple, lorsque vous ne vous souciez pas des permissions du système de fichiers (soit vous n'êtes pas exécuté en tant que Root, soit vous êtes exécuté en tant que Root et le fichier appartient à Root) :

#!/usr/bin/env ruby

require 'tempfile'

def file_edit(filename, regexp, replacement)
  Tempfile.open(".#{File.basename(filename)}", File.dirname(filename)) do |tempfile|
    File.open(filename).each do |line|
      tempfile.puts line.gsub(regexp, replacement)
    end
    tempfile.close
    FileUtils.mv tempfile.path, filename
  end
end

file_edit('/tmp/foo', /foo/, "baz")

TL;DR : Cela devrait être utilisé au lieu de la réponse acceptée au minimum, dans tous les cas, afin de garantir que la mise à jour est atomique et que les lecteurs simultanés ne verront pas de fichiers tronqués. Comme je l'ai mentionné ci-dessus, créer le fichier temporaire dans le même répertoire que le fichier édité est important ici pour éviter que les opérations mv entre périphériques soient traduites en opérations cp si /tmp est monté sur un périphérique différent. L'appel à fdatasync est une couche supplémentaire de paranoïa, mais il entraîne une baisse de performance, donc je l'ai omis dans cet exemple car il n'est pas couramment utilisé.

0 votes

Au lieu d'ouvrir un fichier temporaire dans le répertoire où vous vous trouvez, il en créera automatiquement un dans le répertoire de données de l'application (sous Windows en tout cas) et à partir de là, vous pourrez faire un file.unlink pour le supprimer

3 votes

J'ai vraiment apprécié la réflexion supplémentaire qui a été mise en place. En tant que débutant, il est très intéressant de voir les schémas de pensée de développeurs expérimentés qui peuvent non seulement répondre à la question originale, mais aussi commenter le contexte plus large de ce que la question originale signifie réellement.

1 votes

Programmer, ce n'est pas seulement résoudre le problème immédiat, c'est aussi penser à l'avenir pour éviter d'autres problèmes à venir. Rien n'irrite plus un développeur expérimenté que de rencontrer du code qui a mis l'algorithme dans une impasse, l'obligeant à faire des compromis maladroits, alors qu'un ajustement mineur plus tôt aurait permis d'obtenir un flux agréable. Il faut souvent des heures, voire des jours, d'analyse pour comprendre l'objectif, puis quelques lignes remplacent une page de vieux code. Parfois, c'est comme une partie d'échecs contre les données et le système.

12voto

sepp2k Points 157757

Il n'y a pas vraiment de moyen de modifier les fichiers sur place. Ce que vous faites habituellement lorsque vous pouvez vous en tirer (c'est-à-dire si les fichiers ne sont pas trop gros), c'est lire le fichier en mémoire ( File.read ), effectuez vos substitutions sur la chaîne lue ( String#gsub ) et ensuite réécrire la chaîne modifiée dans le fichier ( File.open , File#write ).

Si les fichiers sont suffisamment volumineux pour que cela ne soit pas réalisable, ce que vous devez faire, c'est lire le fichier par morceaux (si le motif que vous voulez remplacer ne s'étend pas sur plusieurs lignes, alors un morceau correspond généralement à une ligne - vous pouvez utiliser File.foreach pour lire un fichier ligne par ligne), et pour chaque morceau, effectuer la substitution sur celui-ci et l'ajouter à un fichier temporaire. Lorsque vous avez fini d'itérer sur le fichier source, vous le fermez et utilisez la commande FileUtils.mv pour l'écraser avec le fichier temporaire.

1 votes

J'aime l'approche du streaming. Nous traitons de gros fichiers en même temps et nous n'avons généralement pas la place en RAM pour lire le fichier entier.

0 votes

" Pourquoi le "slurp" d'un dossier n'est-il pas une bonne pratique ? "pourrait être une lecture utile à cet égard.

6voto

Tanner Welsh Points 71

Voici une solution pour rechercher/remplacer dans tous les fichiers d'un répertoire donné. En gros, j'ai pris la réponse fournie par sepp2k et je l'ai étendue.

# First set the files to search/replace in
files = Dir.glob("/PATH/*")

# Then set the variables for find/replace
@original_string_or_regex = /REGEX/
@replacement_string = "STRING"

files.each do |file_name|
  text = File.read(file_name)
  replace = text.gsub!(@original_string_or_regex, @replacement_string)
  File.open(file_name, "w") { |file| file.puts replace }
end

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