Rechercher et remplacer une ligne dans un fichier en Python

293

Je veux parcourir le contenu d'un fichier texte et faire une recherche et remplacer sur certaines lignes et écrire le résultat dans le fichier. Je pourrais d'abord charger le fichier entier en mémoire puis le réécrire, mais ce n'est probablement pas la meilleure façon de le faire.

Quelle est la meilleure façon de procéder, dans le code suivant?

f = open(file)
for line in f:
    if line.contains('foo'):
        newline = line.replace('foo', 'bar')
        # how to write this newline back to the file
pkit
la source

Réponses:

192

Je suppose que quelque chose comme ça devrait le faire. Il écrit essentiellement le contenu dans un nouveau fichier et remplace l'ancien fichier par le nouveau fichier:

from tempfile import mkstemp
from shutil import move, copymode
from os import fdopen, remove

def replace(file_path, pattern, subst):
    #Create temp file
    fh, abs_path = mkstemp()
    with fdopen(fh,'w') as new_file:
        with open(file_path) as old_file:
            for line in old_file:
                new_file.write(line.replace(pattern, subst))
    #Copy the file permissions from the old file to the new file
    copymode(file_path, abs_path)
    #Remove original file
    remove(file_path)
    #Move new file
    move(abs_path, file_path)
Thomas Watnedal
la source
5
Juste un petit commentaire: filec'est l'observation d'une classe prédéfinie du même nom.
ezdazuzena
4
Ce code modifie les autorisations sur le fichier d'origine. Comment puis-je conserver les autorisations d'origine?
nic
1
quel est l'intérêt de fh, vous l'utilisez dans l'appel proche mais je ne vois pas l'intérêt de créer un fichier juste pour le fermer ...
Wicelo
2
@Wicelo Vous devez le fermer pour éviter une fuite du descripteur de fichier. Voici une explication décente: logilab.org/17873
Thomas Watnedal
1
Oui, j'ai découvert que cela mkstemp()renvoyait un double et (fh, abs_path) = fh, abs_pathje ne le savais pas quand j'ai posé la question.
Wicelo
272

Le moyen le plus court serait probablement d'utiliser le module d'entrée de fichier . Par exemple, ce qui suit ajoute des numéros de ligne à un fichier, sur place:

import fileinput

for line in fileinput.input("test.txt", inplace=True):
    print('{} {}'.format(fileinput.filelineno(), line), end='') # for Python 3
    # print "%d: %s" % (fileinput.filelineno(), line), # for Python 2

Ce qui se passe ici est:

  1. Le fichier d'origine est déplacé vers un fichier de sauvegarde
  2. La sortie standard est redirigée vers le fichier d'origine dans la boucle
  3. Ainsi, toutes les printinstructions sont réécrites dans le fichier d'origine

fileinputa plus de cloches et de sifflets. Par exemple, il peut être utilisé pour fonctionner automatiquement sur tous les fichiers de sys.args[1:], sans que vous ayez à les parcourir explicitement. À partir de Python 3.2, il fournit également un gestionnaire de contexte pratique à utiliser dans une withinstruction.


Bien qu'il fileinputsoit idéal pour les scripts jetables, je me méfierais de l'utiliser dans du vrai code car il est vrai qu'il n'est pas très lisible ou familier. Dans le code réel (de production), il vaut la peine de dépenser quelques lignes de code supplémentaires pour rendre le processus explicite et ainsi rendre le code lisible.

Il y a deux options:

  1. Le fichier n'est pas trop volumineux et vous pouvez simplement le lire entièrement en mémoire. Fermez ensuite le fichier, rouvrez-le en mode écriture et réécrivez le contenu modifié.
  2. Le fichier est trop volumineux pour être stocké en mémoire; vous pouvez le déplacer vers un fichier temporaire et l'ouvrir, en le lisant ligne par ligne, en le réécrivant dans le fichier d'origine. Notez que cela nécessite deux fois le stockage.
Eli Bendersky
la source
13
Je sais que cela ne comporte que deux lignes, mais je ne pense pas que le code soit très expressif en soi. Parce que si vous réfléchissez un instant, si vous ne connaissiez pas la fonction, il y a très peu d'indices dans ce qui se passe. Imprimer le numéro de ligne et la ligne n'est pas la même chose que l'écrire ... si vous obtenez mon essentiel ...
chutsu
14
Cette DOES écriture au fichier. Il redirige stdout vers le fichier. Jetez un oeil à la documentation
brice
32
Le bit clé ici est la virgule à la fin de l'instruction print: elle surpresse l'instruction print en ajoutant une autre nouvelle ligne (car la ligne en a déjà une). Ce n'est pas très évident du tout, cependant (c'est pourquoi Python 3 a changé cette syntaxe, heureusement).
VPeric
4
Veuillez noter que cela ne fonctionne pas lorsque vous fournissez un crochet d'ouverture au fichier, par exemple lorsque vous essayez de lire / écrire des fichiers encodés en UTF-16.
bompf
5
Pour python3,print(line, end='')
Ch.Idea
80

Voici un autre exemple qui a été testé et qui correspondra aux modèles de recherche et de remplacement:

import fileinput
import sys

def replaceAll(file,searchExp,replaceExp):
    for line in fileinput.input(file, inplace=1):
        if searchExp in line:
            line = line.replace(searchExp,replaceExp)
        sys.stdout.write(line)

Exemple d'utilisation:

replaceAll("/fooBar.txt","Hello\sWorld!$","Goodbye\sWorld.")
Jason
la source
23
L'utilisation exemple fournit une expression régulière, mais ni searchExp in linene line.replacesont des opérations d'expression régulière. Certes, l'exemple d'utilisation est faux.
kojiro
Au lieu de if searchExp in line: line = line.replace(searchExp, replaceExpr)vous, vous pouvez simplement écrire line = line.replace(searchExp, replaceExpr). Aucune exception n'est générée, la ligne reste inchangée.
David Wallace du
A parfaitement fonctionné pour moi aussi. J'étais tombé sur un certain nombre d'autres exemples qui ressemblaient beaucoup à cela, mais l'astuce était l'utilisation de la sys.stdout.write(line). Merci encore!
Sage
Si j'utilise ceci, mon fichier devient vierge. Une idée?
Javier López Tomás
J'utilise ceci
Rakib Fiha
64

Cela devrait fonctionner: (édition sur place)

import fileinput

# Does a list of files, and
# redirects STDOUT to the file in question
for line in fileinput.input(files, inplace = 1): 
      print line.replace("foo", "bar"),
Kinlan
la source
5
+1. Aussi, si vous recevez un RuntimeError: input () déjà actif, appelez le fileinput.close ()
geographika
1
Notez qu'il filesdoit s'agir d'une chaîne contenant le nom du fichier et non d'un objet fichier .
atomh33ls
9
print ajoute une nouvelle ligne qui pourrait déjà exister. pour éviter cela, ajoutez .rstrip () à la fin de vos remplacements
Guillaume Gendre
Utilisez plutôt les fichiers arg dans input (), il pourrait s'agir de fileinput.input (inplace = 1) et appelez le script comme> python replace.py myfiles * .txt
chespinoza
24

Basé sur la réponse de Thomas Watnedal. Cependant, cela ne répond pas exactement à la partie ligne à ligne de la question d'origine. La fonction peut toujours être remplacée ligne par ligne

Cette implémentation remplace le contenu du fichier sans utiliser de fichiers temporaires, par conséquent les autorisations de fichier restent inchangées.

Aussi re.sub au lieu de replace, permet le remplacement regex au lieu du remplacement en texte brut uniquement.

La lecture du fichier sous la forme d'une chaîne unique au lieu d'une ligne par ligne permet une correspondance et un remplacement sur plusieurs lignes.

import re

def replace(file, pattern, subst):
    # Read contents from file as a single string
    file_handle = open(file, 'r')
    file_string = file_handle.read()
    file_handle.close()

    # Use RE package to allow for replacement (also allowing for (multiline) REGEX)
    file_string = (re.sub(pattern, subst, file_string))

    # Write contents to file.
    # Using mode 'w' truncates the file.
    file_handle = open(file, 'w')
    file_handle.write(file_string)
    file_handle.close()
Thijs
la source
2
Vous voudrez peut-être utiliser les attributs rbet wblors de l'ouverture des fichiers car cela préservera les fins de ligne d'origine
Nux
Dans Python 3, vous ne pouvez pas utiliser 'wb' et 'rb' avec 're'. Il donnera l'erreur "TypeError: impossible d'utiliser un modèle de chaîne sur un objet semblable à des octets"
15

Comme le suggère lassevk, écrivez le nouveau fichier au fur et à mesure, voici un exemple de code:

fin = open("a.txt")
fout = open("b.txt", "wt")
for line in fin:
    fout.write( line.replace('foo', 'bar') )
fin.close()
fout.close()
hamishmcn
la source
12

Si vous voulez une fonction générique qui remplace n'importe quel texte par un autre texte, c'est probablement la meilleure façon de procéder, en particulier si vous êtes un fan des expressions régulières:

import re
def replace( filePath, text, subs, flags=0 ):
    with open( filePath, "r+" ) as file:
        fileContents = file.read()
        textPattern = re.compile( re.escape( text ), flags )
        fileContents = textPattern.sub( subs, fileContents )
        file.seek( 0 )
        file.truncate()
        file.write( fileContents )
starryknight64
la source
12

Une manière plus pythonique serait d'utiliser des gestionnaires de contexte comme le code ci-dessous:

from tempfile import mkstemp
from shutil import move
from os import remove

def replace(source_file_path, pattern, substring):
    fh, target_file_path = mkstemp()
    with open(target_file_path, 'w') as target_file:
        with open(source_file_path, 'r') as source_file:
            for line in source_file:
                target_file.write(line.replace(pattern, substring))
    remove(source_file_path)
    move(target_file_path, source_file_path)

Vous pouvez trouver l'extrait complet ici .

Kiran
la source
En Python> = 3.1, vous pouvez ouvrir les deux gestionnaires de contexte sur la même ligne .
florisla
4

Créez un nouveau fichier, copiez les lignes de l'ancien vers le nouveau et effectuez le remplacement avant d'écrire les lignes dans le nouveau fichier.

Lasse V. Karlsen
la source
4

En développant la réponse de @ Kiran, qui, je suis d'accord, est plus succincte et Pythonic, cela ajoute des codecs pour prendre en charge la lecture et l'écriture de l'UTF-8:

import codecs 

from tempfile import mkstemp
from shutil import move
from os import remove


def replace(source_file_path, pattern, substring):
    fh, target_file_path = mkstemp()

    with codecs.open(target_file_path, 'w', 'utf-8') as target_file:
        with codecs.open(source_file_path, 'r', 'utf-8') as source_file:
            for line in source_file:
                target_file.write(line.replace(pattern, substring))
    remove(source_file_path)
    move(target_file_path, source_file_path)
igniteflow
la source
Va-t-il conserver l'autorisation de l'ancien fichier dans le nouveau fichier?
Bidyut
2

En utilisant la réponse de hamishmcn comme modèle, j'ai pu rechercher une ligne dans un fichier qui correspond à mon expression régulière et la remplacer par une chaîne vide.

import re 

fin = open("in.txt", 'r') # in file
fout = open("out.txt", 'w') # out file
for line in fin:
    p = re.compile('[-][0-9]*[.][0-9]*[,]|[-][0-9]*[,]') # pattern
    newline = p.sub('',line) # replace matching strings with empty string
    print newline
    fout.write(newline)
fin.close()
fout.close()
Emmanuel
la source
1
Vous devez compiler l'expression régulière à l'extérieur de la boucle for, sinon c'est un gaspillage de performance
Axel
2

fileinput est assez simple comme mentionné dans les réponses précédentes:

import fileinput

def replace_in_file(file_path, search_text, new_text):
    with fileinput.input(file_path, inplace=True) as f:
        for line in f:
            new_line = line.replace(search_text, new_text)
            print(new_line, end='')

Explication:

  • fileinputpeut accepter plusieurs fichiers, mais je préfère fermer chaque fichier dès qu'il est en cours de traitement. Donc placé seul file_pathdans la withdéclaration.
  • printL'instruction n'imprime rien quand inplace=True, car elle STDOUTest transmise au fichier d'origine.
  • end=''dans l' printinstruction est d'éliminer les nouvelles lignes vierges intermédiaires.

Peut être utilisé comme suit:

file_path = '/path/to/my/file'
replace_in_file(file_path, 'old-text', 'new-text')
Akif
la source
0

si vous supprimez le retrait comme ci-dessous, il recherchera et remplacera sur plusieurs lignes. Voir ci-dessous par exemple.

def replace(file, pattern, subst):
    #Create temp file
    fh, abs_path = mkstemp()
    print fh, abs_path
    new_file = open(abs_path,'w')
    old_file = open(file)
    for line in old_file:
        new_file.write(line.replace(pattern, subst))
    #close temp file
    new_file.close()
    close(fh)
    old_file.close()
    #Remove original file
    remove(file)
    #Move new file
    move(abs_path, file)
loi
la source
Le formatage de ce code Python ne semble pas tout à fait correct ... (J'ai essayé de corriger, mais je ne savais pas ce qui était prévu)
Andy Hayden