Pesquise e substitua uma linha em um arquivo em Python

293

Quero fazer um loop sobre o conteúdo de um arquivo de texto, fazer uma pesquisa e substituir em algumas linhas e escrever o resultado novamente no arquivo. Eu poderia primeiro carregar o arquivo inteiro na memória e depois escrevê-lo novamente, mas essa provavelmente não é a melhor maneira de fazê-lo.

Qual é a melhor maneira de fazer isso, dentro do código a seguir?

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
fonte

Respostas:

191

Eu acho que algo assim deveria fazer isso. Basicamente, grava o conteúdo em um novo arquivo e substitui o arquivo antigo pelo novo:

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
fonte
5
Apenas um pequeno comentário: fileestá sombreando uma classe predefinida com o mesmo nome.
Ezdazuzena
4
Este código altera as permissões no arquivo original. Como posso manter as permissões originais?
nic
1
que é o ponto de fh, você usá-lo na chamada perto, mas eu não vejo o ponto de criar um arquivo apenas para fechá-lo ...
Wicelo
2
@Wicelo Você precisa fechá-lo para evitar vazamentos do descritor de arquivo. Aqui está uma explicação decente: logilab.org/17873
Thomas Watnedal
1
Sim, eu descobri que mkstemp()está retornando uma tupla de 2 e (fh, abs_path) = fh, abs_path, eu não sabia disso quando fiz a pergunta.
Wicelo 20/09/14
272

A maneira mais curta provavelmente seria usar o módulo fileinput . Por exemplo, o seguinte adiciona números de linha a um arquivo no local:

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

O que acontece aqui é:

  1. O arquivo original é movido para um arquivo de backup
  2. A saída padrão é redirecionada para o arquivo original dentro do loop
  3. Portanto, qualquer printinstrução grava de volta no arquivo original

fileinputtem mais sinos e assobios. Por exemplo, ele pode ser usado para operar automaticamente todos os arquivos sys.args[1:], sem que você precise iterar explicitamente sobre eles. A partir do Python 3.2, ele também fornece um gerenciador de contexto conveniente para uso em uma withinstrução.


Enquanto fileinput seja ótimo para scripts descartáveis, eu seria cauteloso em usá-lo em código real, porque é certo que não é muito legível ou familiar. No código real (de produção), vale a pena gastar apenas mais algumas linhas de código para tornar o processo explícito e, assim, tornar o código legível.

Existem duas opções:

  1. O arquivo não é muito grande e você pode simplesmente lê-lo na memória. Em seguida, feche o arquivo, reabra-o no modo de gravação e escreva o conteúdo modificado novamente.
  2. O arquivo é muito grande para ser armazenado na memória; você pode movê-lo para um arquivo temporário e abri-lo, lendo-o linha por linha, escrevendo novamente no arquivo original. Observe que isso requer o dobro do armazenamento.
Eli Bendersky
fonte
13
Eu sei que isso tem apenas duas linhas, no entanto, não acho que o código seja muito expressivo em si. Porque se você pensa por um segundo, se não conhece a função, há muito poucas pistas sobre o que está acontecendo. Imprimir o número da linha ea linha não é o mesmo que escrever isso ... se você pegar minha essência ...
chutsu
14
Este FAZ gravação para o arquivo. Ele redireciona o stdout para o arquivo. Dê uma olhada nos documentos
brice
32
O ponto principal aqui é a vírgula no final da instrução print: ela substitui a instrução print adicionando outra nova linha (como a linha já possui uma). Porém, não é muito óbvio (é por isso que o Python 3 mudou essa sintaxe, por sorte).
VPeric
4
Observe que isso não funciona quando você fornece um gancho de abertura para o arquivo, por exemplo, ao tentar ler / gravar arquivos codificados em UTF-16.
Bompf 01/07
5
Para python3,print(line, end='')
Ch.Idea
80

Aqui está outro exemplo que foi testado e corresponderá aos padrões de pesquisa e substituição:

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)

Exemplo de uso:

replaceAll("/fooBar.txt","Hello\sWorld!$","Goodbye\sWorld.")
Jason
fonte
23
O exemplo de uso fornece uma expressão regular, mas também searchExp in linenão line.replacesão operações de expressão regular. Certamente o exemplo de uso está errado.
Kojiro
Em vez de if searchExp in line: line = line.replace(searchExp, replaceExpr)você pode apenas escrever line = line.replace(searchExp, replaceExpr). Nenhuma exceção é gerada, a linha permanece inalterada.
David Wallace
Funcionou perfeitamente para mim também. Eu já havia encontrado vários outros exemplos muito parecidos com isso, mas o truque era o uso do sys.stdout.write(line). Obrigado novamente!
Sábio
Se eu usar isso, meu arquivo ficará em branco. Qualquer ideia?
Javier López Tomás
Estou usando isso
Rakib Fiha 20/01
64

Isso deve funcionar: (edição no local)

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
fonte
5
+1. Além disso, se você receber um RuntimeError: input () já ativo, chame o fileinput.close ()
geographika
1
Observe que filesdeve ser uma sequência que contém o nome do arquivo, não um objeto de arquivo .
atomh33ls
9
O print adiciona uma nova linha que já pode estar lá. para evitar isso, adicione .rstrip () no final de suas substituições #
Guillaume Gendre
Em vez disso usar arquivos de arg em input (), poderia ser fileinput.input (inplace = 1) e chamar o script como> python replace.py myfiles * .txt
chespinoza
24

Com base na resposta de Thomas Watnedal. No entanto, isso não responde exatamente à parte linha a linha da pergunta original. A função ainda pode substituir linha a linha

Esta implementação substitui o conteúdo do arquivo sem usar arquivos temporários, como conseqüência, as permissões do arquivo permanecem inalteradas.

Também re.sub, em vez de substituir, permite a substituição de regex em vez de apenas substituição de texto sem formatação.

A leitura do arquivo como uma única sequência de caracteres, em vez de linha por linha, permite a correspondência e substituição de várias linhas.

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
fonte
2
Você pode querer usar rbe wbatribui ao abrir arquivos, pois isso irá preservar finais de linha originais
Nux
No Python 3, você não pode usar 'wb' e 'rb' com 're'. Ele dará o erro "TypeError: não é possível usar um padrão de seqüência de caracteres em um objeto semelhante a bytes"
15

Como sugere o lassevk, escreva o novo arquivo à medida que avança, aqui está um exemplo de código:

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

Se você deseja uma função genérica que substitua qualquer texto por outro, provavelmente é o melhor caminho a seguir, principalmente se você é fã do regex:

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

Uma maneira mais pitônica seria usar gerenciadores de contexto como o código abaixo:

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)

Você pode encontrar o snippet completo aqui .

Kiran
fonte
No Python> = 3.1, você pode abrir os dois gerenciadores de contexto na mesma linha .
Florisla # 6/18
4

Crie um novo arquivo, copie as linhas do antigo para o novo e faça a substituição antes de gravar as linhas no novo arquivo.

Lasse V. Karlsen
fonte
4

Expandindo a resposta de @ Kiran, que eu concordo é mais sucinta e Pythonic, isso adiciona codecs para apoiar a leitura e gravação 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
fonte
Isso preservará a permissão do arquivo antigo no novo arquivo?
precisa saber é o seguinte
2

Usando a resposta de hamishmcn como modelo, fui capaz de procurar uma linha em um arquivo que corresponda ao meu regex e substituí-lo por uma string vazia.

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
fonte
1
Você deve compilar o FORA regex o loop for, caso contrário, é um desperdício de desempenho
Axel
2

fileinput é bastante direto, como mencionado nas respostas 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='')

Explicação:

  • fileinputpode aceitar vários arquivos, mas prefiro fechar cada arquivo assim que estiver sendo processado. Então, colocado único file_pathna withdeclaração.
  • printA instrução não imprime nada quando inplace=True, porque STDOUTestá sendo encaminhada para o arquivo original.
  • end=''na printdeclaração é eliminar novas linhas intermediárias em branco.

Pode ser usado da seguinte maneira:

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

se você remover o recuo como abaixo, ele pesquisará e substituirá em várias linhas. Veja abaixo, por exemplo.

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
fonte
A formatação deste código Python não parece muito certo ... (Eu tentei corrigir, mas não tinha certeza de que se pretendia)
Andy Hayden