Wyszukaj i zamień wiersz w pliku w Pythonie

292

Chcę zapętlić zawartość pliku tekstowego, wyszukać i zamienić w niektórych wierszach i zapisać wynik z powrotem do pliku. Mogłem najpierw załadować cały plik do pamięci, a następnie zapisać go z powrotem, ale prawdopodobnie nie jest to najlepszy sposób na zrobienie tego.

Jak najlepiej to zrobić w ramach następującego kodu?

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
źródło

Odpowiedzi:

191

Myślę, że coś takiego powinno to zrobić. Zasadniczo zapisuje zawartość do nowego pliku i zastępuje stary plik nowym plikiem:

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
źródło
5
Drobny komentarz: filecienie predefiniowanej klasy o tej samej nazwie.
ezdazuzena
4
Ten kod zmienia uprawnienia do oryginalnego pliku. Jak mogę zachować oryginalne uprawnienia?
nic.
1
jaki jest sens fh, używasz go w zamkniętym wywołaniu, ale nie widzę sensu tworzenia pliku tylko po to, aby go zamknąć ...
Wicelo
2
@Wicelo Musisz go zamknąć, aby zapobiec wyciekom deskryptora pliku. Oto przyzwoite wyjaśnienie: logilab.org/17873
Thomas Watnedal
1
Tak, odkryłem, że mkstemp()zwraca 2-krotkę i (fh, abs_path) = fh, abs_pathnie wiedziałem o tym, kiedy zadałem pytanie.
Wicelo,
271

Najkrótszym sposobem byłoby prawdopodobnie użycie modułu fileinput . Na przykład poniższe dodaje w miejscu numery wierszy:

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

To, co się tutaj dzieje, to:

  1. Oryginalny plik zostanie przeniesiony do pliku kopii zapasowej
  2. Standardowe wyjście jest przekierowywane do oryginalnego pliku w pętli
  3. Zatem wszelkie printinstrukcje zapisują się z powrotem w oryginalnym pliku

fileinputma więcej dzwonków i gwizdków. Na przykład, można go użyć do automatycznego działania na wszystkich plikach wewnątrz sys.args[1:], bez konieczności jawnej iteracji nad nimi. Począwszy od Python 3.2, zapewnia on także wygodny menedżer kontekstu do użycia w withinstrukcji.


Podczas fileinput świetnie nadaje się do skryptów typu „wyrzucaj”, nie chciałbym używać go w prawdziwym kodzie, ponieważ, co prawda, nie jest on zbyt czytelny ani znajomy. W prawdziwym (produkcyjnym) kodzie warto wydać jeszcze kilka wierszy kodu, aby proces był jawny, a przez to czytelny.

Istnieją dwie opcje:

  1. Plik nie jest zbyt duży i można go w całości odczytać do pamięci. Następnie zamknij plik, otwórz go ponownie w trybie pisania i zapisz zmodyfikowaną zawartość z powrotem.
  2. Plik jest zbyt duży, aby mógł zostać zapisany w pamięci; możesz przenieść go do pliku tymczasowego i otworzyć, czytając go wiersz po wierszu, zapisując z powrotem do oryginalnego pliku. Pamiętaj, że wymaga to dwa razy więcej miejsca.
Eli Bendersky
źródło
13
Wiem, że zawiera tylko dwie linie, jednak nie sądzę, aby kod sam w sobie był bardzo wyrazisty. Ponieważ jeśli myślisz przez chwilę, jeśli nie znasz tej funkcji, jest bardzo mało wskazówek na temat tego, co się dzieje. Drukowanie numeru linii i linia nie jest tym samym, co pisanie ... jeśli dostaniesz moją treść ...
chutsu
14
Ten CZY zapisu pliku. Przekierowuje standardowe wyjście do pliku. Spójrz na dokumenty
Brice
32
Kluczem tutaj jest przecinek na końcu instrukcji print: nie wyświetla instrukcji print, dodając kolejny znak nowej linii (ponieważ linia już ją ma). Nie jest to jednak wcale oczywiste (dlatego na szczęście Python 3 zmienił tę składnię).
VPeric
4
Zauważ, że to nie działa, gdy otworzysz zaczep do pliku, np. Podczas próby odczytu / zapisu plików zakodowanych w UTF-16.
bompf
5
Dla python3,print(line, end='')
Ch.Idea
80

Oto kolejny przykład, który został przetestowany i będzie pasował do wzorców wyszukiwania i zamiany:

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)

Przykładowe zastosowanie:

replaceAll("/fooBar.txt","Hello\sWorld!$","Goodbye\sWorld.")
Jason
źródło
23
Przykład użycia zapewnia wyrażenie regularne, ale ani searchExp in linenie line.replacesą operacjami wyrażeń regularnych. Z pewnością użycie przykładu jest nieprawidłowe.
kojiro
Zamiast tego if searchExp in line: line = line.replace(searchExp, replaceExpr)możesz po prostu pisać line = line.replace(searchExp, replaceExpr). Nie jest generowany żaden wyjątek, linia pozostaje niezmieniona.
David Wallace,
Dla mnie też działało idealnie. Natknąłem się na wiele innych przykładów, które wyglądały bardzo podobnie do tego, ale sztuczką było użycie sys.stdout.write(line). Dzięki jeszcze raz!
Sage,
Jeśli tego użyję, mój plik stanie się pusty. Dowolny pomysł?
Javier López Tomás,
Używam tego
Rakib Fiha
64

To powinno działać: (edycja w miejscu)

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
źródło
5
+1. Również jeśli RuntimeError: input () jest już aktywny, to wywołaj fileinput.close ()
geographika
1
Pamiętaj, że filespowinien to być ciąg znaków zawierający nazwę pliku, a nie obiekt pliku .
atomh33ls
9
print dodaje nowy wiersz, który może już tam być. aby tego uniknąć, dodaj .rstrip () na końcu zamienników
Guillaume Gendre
Zamiast tego użyj argumentu argument w input (), może to być fileinput.input (inplace = 1) i wywołać skrypt jako> python replace.py myfiles * .txt
chespinoza
24

Na podstawie odpowiedzi Thomasa Watnedala. Nie odpowiada to jednak dokładnie na pytanie liniowe w pierwotnym pytaniu. Funkcja może nadal zastępować liniowo

Ta implementacja zastępuje zawartość pliku bez użycia plików tymczasowych, w wyniku czego uprawnienia do plików pozostają niezmienione.

Również re.sub zamiast replace pozwala na zamianę wyrażeń regularnych zamiast zamiany zwykłego tekstu.

Odczytywanie pliku jako pojedynczego ciągu zamiast wiersza po wierszu pozwala na dopasowanie wielu wierszy i zamianę.

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
źródło
2
Możesz chcieć użyć rbi wbatrybutów podczas otwierania plików, ponieważ pozwoli to zachować oryginalne zakończenia linii
Nux
W Pythonie 3 nie można używać „wb” i „rb” z „re”. Daje błąd „TypeError: nie można użyć wzorca ciągu na obiekcie podobnym do bajtów”
15

Jak sugeruje lassevk, zapisz nowy plik na bieżąco, oto przykładowy kod:

fin = open("a.txt")
fout = open("b.txt", "wt")
for line in fin:
    fout.write( line.replace('foo', 'bar') )
fin.close()
fout.close()
hamishmcn
źródło
12

Jeśli potrzebujesz ogólnej funkcji, która zastępuje dowolny tekst innym tekstem, jest to prawdopodobnie najlepsza droga, szczególnie jeśli jesteś fanem wyrażeń regularnych:

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
źródło
12

Bardziej pythonowym sposobem byłoby użycie menedżerów kontekstu, takich jak poniższy kod:

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)

Pełny fragment kodu można znaleźć tutaj .

Kiran
źródło
W Pythonie> = 3.1 można otworzyć dwa menedżery kontekstów w tej samej linii .
florisla
4

Utwórz nowy plik, skopiuj linie ze starego do nowego i wykonaj zamianę przed zapisaniem linii do nowego pliku.

Lasse V. Karlsen
źródło
4

Rozwijając odpowiedź @ Kirana, którą zgadzam się, jest bardziej zwięzła i Pythonic, dodaje to kodeki do obsługi odczytu i zapisu 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
źródło
Czy zachowa uprawnienia starego pliku do nowego pliku?
Bidyut
2

Wykorzystując odpowiedź hamishmcn jako szablon, byłem w stanie wyszukać wiersz w pliku, który pasuje do mojego wyrażenia regularnego i zastąpić go pustym ciągiem.

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
źródło
1
Powinieneś skompilować wyrażenie regularne POZA pętlą for, w przeciwnym razie marnujesz wydajność
Axel
2

fileinput jest dość proste, jak wspomniano w poprzednich odpowiedziach:

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

Wyjaśnienie:

  • fileinputmogę zaakceptować wiele plików, ale wolę zamknąć każdy pojedynczy plik, gdy tylko zostanie przetworzony. Tak umieszczony singielfile_path w withinstrukcji.
  • print instrukcja nie drukuje niczego, gdy inplace=True , ponieważSTDOUT jest przekazywana do oryginalnego pliku.
  • end='' w print instrukcji jest wyeliminowanie pośrednich pustych nowych wierszy.

Może być używany w następujący sposób:

file_path = '/path/to/my/file'
replace_in_file(file_path, 'old-text', 'new-text')
Akif
źródło
0

jeśli usuniesz wcięcie w podobny sposób poniżej, przeszuka ono i zastąpi wiele wierszy. Zobacz na przykład poniżej.

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
źródło
Formatowanie tego kodu w Pythonie nie wygląda całkiem dobrze ... (Próbowałem to naprawić, ale nie byłem pewien, co było zamierzone)
Andy Hayden