Buscar y reemplazar una línea en un archivo en Python

293

Quiero recorrer el contenido de un archivo de texto y hacer una búsqueda y reemplazar en algunas líneas y escribir el resultado en el archivo. Primero podría cargar todo el archivo en la memoria y luego volver a escribirlo, pero probablemente esa no sea la mejor manera de hacerlo.

¿Cuál es la mejor manera de hacer esto, dentro del siguiente código?

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
fuente

Respuestas:

192

Supongo que algo como esto debería hacerlo. Básicamente, escribe el contenido en un nuevo archivo y reemplaza el archivo antiguo con el nuevo archivo:

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
fuente
55
Solo un comentario menor: fileestá sombreando una clase predefinida del mismo nombre.
ezdazuzena
44
Este código cambia los permisos en el archivo original. ¿Cómo puedo mantener los permisos originales?
nic
1
cuál es el punto de fh, lo usas en la llamada cercana pero no veo el punto de crear un archivo solo para cerrarlo ...
Wicelo
2
@Wicelo Debe cerrarlo para evitar fugas del descriptor de archivo. Aquí hay una explicación decente: logilab.org/17873
Thomas Watnedal
1
Sí, descubrí que mkstemp()está devolviendo una tupla de 2 y (fh, abs_path) = fh, abs_pathno lo sabía cuando hice la pregunta.
Wicelo
272

La forma más corta probablemente sería usar el módulo de entrada de archivo . Por ejemplo, lo siguiente agrega números de línea a un archivo, en el lugar:

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

Lo que pasa aquí es:

  1. El archivo original se mueve a un archivo de respaldo
  2. La salida estándar se redirige al archivo original dentro del bucle
  3. Por lo tanto, cualquier printdeclaración vuelve a escribirse en el archivo original

fileinputTiene más campanas y silbatos. Por ejemplo, se puede usar para operar automáticamente en todos los archivos sys.args[1:], sin tener que repetirlos explícitamente. Comenzando con Python 3.2, también proporciona un administrador de contexto conveniente para usar en una withdeclaración.


Si bien fileinputes excelente para los scripts desechables, desconfiaría de usarlo en código real porque es cierto que no es muy legible o familiar. En el código real (de producción) vale la pena gastar unas pocas líneas de código más para hacer que el proceso sea explícito y, por lo tanto, hacer que el código sea legible.

Hay dos opciones:

  1. El archivo no es demasiado grande y puede leerlo completamente en la memoria. Luego cierre el archivo, vuelva a abrirlo en modo de escritura y vuelva a escribir el contenido modificado.
  2. El archivo es demasiado grande para ser almacenado en la memoria; puede moverlo a un archivo temporal y abrirlo, leyéndolo línea por línea, volviendo a escribirlo en el archivo original. Tenga en cuenta que esto requiere el doble de almacenamiento.
Eli Bendersky
fuente
13
Sé que esto solo tiene dos líneas, sin embargo, no creo que el código sea muy expresivo en sí mismo. Porque si piensa por un segundo, si no conocía la función, hay muy pocas pistas sobre lo que está sucediendo. Imprimir el número de línea y la línea no es lo mismo que escribir que ... no sé si me lo esencial ...
Chutsu
14
Este HACE escritura en el archivo. Redirige stdout al archivo. Echa un vistazo a los documentos
brice
32
La clave aquí es la coma al final de la declaración de impresión: suprime la declaración de impresión agregando otra nueva línea (ya que la línea ya tiene una). Sin embargo, no es muy obvio (por eso Python 3 cambió esa sintaxis, por suerte).
VPeric
44
Tenga en cuenta que esto no funciona cuando proporciona un enlace de apertura al archivo, por ejemplo, cuando intenta leer / escribir archivos codificados UTF-16.
bompf 01 de
55
Para python3,print(line, end='')
Ch.Idea
80

Aquí hay otro ejemplo que se probó y que coincidirá con los patrones de búsqueda y reemplazo:

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)

Ejemplo de uso:

replaceAll("/fooBar.txt","Hello\sWorld!$","Goodbye\sWorld.")
Jason
fuente
23
El uso de ejemplo proporciona una expresión regular, pero searchExp in linetampoco lo line.replaceson las operaciones de expresión regular. Seguramente el uso del ejemplo es incorrecto.
kojiro
En lugar de if searchExp in line: line = line.replace(searchExp, replaceExpr)ti solo puedes escribir line = line.replace(searchExp, replaceExpr). No se genera ninguna excepción, la línea simplemente permanece sin cambios.
David Wallace
Funcionó perfectamente para mí también. Me encontré con otros ejemplos que se parecían mucho a esto, pero el truco fue el uso de sys.stdout.write(line). ¡Gracias de nuevo!
Sage
Si uso esto, mi archivo se queda en blanco. ¿Alguna idea?
Javier López Tomás
Estoy usando esto
Rakib Fiha
64

Esto debería funcionar: (edición in situ)

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
fuente
55
+1. Además, si recibe un RuntimeError: input () ya activo, llame al fileinput.close ()
geographika
1
Tenga en cuenta que filesdebe ser una cadena que contenga el nombre del archivo, no un objeto de archivo .
atomh33ls
99
print agrega una nueva línea que ya podría estar allí. para evitar esto, agregue .rstrip () al final de sus reemplazos
Guillaume Gendre
En lugar de usar archivos arg en input (), podría ser fileinput.input (inplace = 1) y llamar al script como> python replace.py myfiles * .txt
chespinoza
24

Basado en la respuesta de Thomas Watnedal. Sin embargo, esto no responde exactamente la parte de línea a línea de la pregunta original. La función aún puede reemplazarse de línea a línea

Esta implementación reemplaza el contenido del archivo sin usar archivos temporales, como consecuencia los permisos de archivo permanecen sin cambios.

También re.sub en lugar de reemplazar, permite el reemplazo de expresiones regulares en lugar del reemplazo de texto sin formato solamente.

Leer el archivo como una sola cadena en lugar de línea por línea permite la coincidencia y el reemplazo de varias líneas.

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
fuente
2
Es posible que desee utilizar rby wbatributos al abrir archivos, ya que esto preservará las terminaciones de línea originales
Nux
En Python 3, no puedes usar 'wb' y 'rb' con 're'. Dará el error "TypeError: no se puede usar un patrón de cadena en un objeto similar a bytes"
15

Como sugiere lassevk, escriba el nuevo archivo a medida que avanza, aquí hay un código de ejemplo:

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

Si desea una función genérica que reemplace cualquier texto con otro texto, esta es probablemente la mejor manera de hacerlo, especialmente si es fanático de las expresiones regulares:

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
fuente
12

Una forma más pitónica sería usar administradores de contexto como el código a continuación:

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)

Puedes encontrar el fragmento completo aquí .

Kiran
fuente
En Python> = 3.1, puede abrir los dos administradores de contexto en la misma línea .
florisla
4

Cree un nuevo archivo, copie las líneas de la antigua a la nueva y reemplace antes de escribir las líneas en el nuevo archivo.

Lasse V. Karlsen
fuente
4

Ampliando la respuesta de @ Kiran, que estoy de acuerdo es más sucinta y pitónica, esto agrega códecs para apoyar la lectura y escritura de 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
fuente
¿Va a preservar el permiso del archivo antiguo en el archivo nuevo?
Bidyut
2

Utilizando la respuesta de hamishmcn como plantilla, pude buscar una línea en un archivo que coincida con mi expresión regular y reemplazarla con una cadena vacía.

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
fuente
1
Debería compilar la expresión regular FUERA del ciclo for, de lo contrario es un desperdicio de rendimiento
Axel
2

fileinput es bastante sencillo como se menciona en las respuestas anteriores:

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='')

Explicación:

  • fileinputpuede aceptar varios archivos, pero prefiero cerrar cada archivo tan pronto como se esté procesando. Así colocado solo file_pathen la withdeclaración.
  • printla declaración no imprime nada cuando inplace=True, porque STDOUTse reenvía al archivo original.
  • end=''en la printdeclaración es eliminar nuevas líneas intermedias en blanco.

Se puede usar de la siguiente manera:

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

si quita la sangría en el siguiente ejemplo, buscará y reemplazará en varias líneas. Ver abajo por ejemplo.

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
fuente
El formato de este código de Python no se ve del todo bien ... (Traté de arreglarlo, pero no estaba seguro de lo que se pretendía)
Andy Hayden