Jak wyszukać tekst w pliku pod kątem wzorca i zamienić go na podaną wartość


117

Szukam skryptu do przeszukiwania pliku (lub listy plików) pod kątem wzorca i, jeśli zostanie znaleziony, zamień ten wzorzec na podaną wartość.

Myśli?


1
W poniższych odpowiedziach pamiętaj, że wszelkie zalecenia, których należy użyć, File.readnależy złagodzić informacjami ze stackoverflow.com/a/25189286/128421, wyjaśniającymi, dlaczego siorbanie dużych plików jest złe. Również zamiast File.open(filename, "w") { |file| file << content }odmian użyj File.write(filename, content).
Tin Man

Odpowiedzi:


190

Zastrzeżenie: To podejście jest naiwną ilustracją możliwości Rubiego, a nie rozwiązaniem do zastępowania ciągów w plikach na poziomie produkcyjnym. Jest podatny na różne scenariusze awarii, takie jak utrata danych w przypadku awarii, przerwania lub zapełnienia dysku. Ten kod nie nadaje się do niczego poza szybkim, jednorazowym skryptem, w którym wszystkie dane są archiwizowane. Z tego powodu NIE kopiuj tego kodu do swoich programów.

Oto krótki krótki sposób, aby to zrobić.

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

Czy umieszcza zmianę z powrotem w pliku? Pomyślałem, że to po prostu wydrukuje zawartość na konsoli.
Dane O'Connor

Tak, drukuje zawartość na konsoli.
sepp2k

7
Tak, nie byłem pewien, czy tego chcesz. Aby pisać użyj File.open (file_name, "w") {| file | file.puts output_of_gsub}
Max Chernyak

7
Musiałem użyć file.write: File.open (nazwa_pliku, "w") {| file | file.write (text)}
austen

3
Aby zapisać plik, zamień wiersz puts naFile.write(file_name, text.gsub(/regexp/, "replace")
ciasny

106

W rzeczywistości Ruby ma funkcję edycji lokalnej. Możesz powiedzieć, jak Perl

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

Spowoduje to zastosowanie kodu w cudzysłowach do wszystkich plików w bieżącym katalogu, których nazwy kończą się na „.txt”. Kopie zapasowe edytowanych plików zostaną utworzone z rozszerzeniem „.bak” (chyba „foobar.txt.bak”).

UWAGA: wygląda na to, że nie działa w przypadku wyszukiwania wielowierszowego. W tym przypadku musisz zrobić to w inny mniej ładny sposób, stosując skrypt opakowujący wokół wyrażenia regularnego.


1
Co to do cholery jest pi.bak? Bez tego pojawia się błąd. -e: 1: in <main>': undefined method gsub 'dla main: Object (NoMethodError)
Ninad

15
@NinadPachpute -iedytuje w miejscu. .bakto rozszerzenie pliku kopii zapasowej (opcjonalnie). -pjest czymś w rodzaju while gets; <script>; puts $_; end. ( $_to ostatnia przeczytana linia, ale możesz przypisać do niej coś w rodzaju echo aa | ruby -p -e '$_.upcase!'.)
Lri

1
To lepsza odpowiedź niż zaakceptowana odpowiedź, IMHO, jeśli chcesz zmodyfikować plik.
Colin K,

6
Jak mogę tego użyć w skrypcie ruby?
Saurabh

1
Istnieje wiele sposobów, w jakie może to się nie udać, więc przetestuj go dokładnie, zanim spróbujesz go z plikiem krytycznym.
Tin Man

49

Pamiętaj, że kiedy to zrobisz, w systemie plików może zabraknąć miejsca i możesz utworzyć plik o zerowej długości. Jest to katastrofalne, jeśli robisz coś takiego jak wypisywanie plików / etc / passwd w ramach zarządzania konfiguracją systemu.

Zwróć uwagę, że edycja plików w miejscu, taka jak w zaakceptowanej odpowiedzi, zawsze spowoduje obcięcie pliku i sekwencyjne wypisanie nowego pliku. Zawsze wystąpi sytuacja wyścigu, w której współbieżni czytelnicy zobaczą obcięty plik. Jeśli proces zostanie przerwany z jakiegokolwiek powodu (ctrl-c, OOM killer, awaria systemu, awaria zasilania itp.) Podczas zapisu, to obcięty plik również zostanie pozostawiony, co może być katastrofalne. Jest to rodzaj scenariusza utraty danych, który programiści MUSZĄ wziąć pod uwagę, ponieważ tak się stanie. Z tego powodu uważam, że zaakceptowana odpowiedź najprawdopodobniej nie powinna być zaakceptowaną odpowiedzią. Jako minimum napisz do pliku tymczasowego i przenieś / zmień nazwę pliku na miejsce, tak jak w „prostym” rozwiązaniu na końcu tej odpowiedzi.

Musisz użyć algorytmu, który:

  1. Odczytuje stary plik i zapisuje do nowego pliku. (Musisz uważać na wrzucanie całych plików do pamięci).

  2. Jawnie zamyka nowy plik tymczasowy, w którym można zgłosić wyjątek, ponieważ nie można zapisać buforów plików na dysku, ponieważ nie ma miejsca. (Złap to i wyczyść plik tymczasowy, jeśli chcesz, ale musisz w tym momencie coś ponownie wrzucić lub dość mocno zawieść.

  3. Naprawia uprawnienia do plików i tryby w nowym pliku.

  4. Zmienia nazwę nowego pliku i umieszcza go na miejscu.

Dzięki systemom plików ext3 masz gwarancję, że metadane zapisywane w celu przeniesienia pliku na miejsce nie zostaną przestawione przez system plików i zapisane przed zapisaniem buforów danych dla nowego pliku, więc powinno to się powieść lub zakończyć niepowodzeniem. System plików ext4 również został załatany, aby obsługiwał tego typu zachowanie. Jeśli jesteś bardzo paranoikiem, powinieneś wywołać wywołanie fdatasync()systemowe w kroku 3.5 przed przeniesieniem pliku na miejsce.

Niezależnie od języka jest to najlepsza praktyka. W językach, w których wywołanie close()nie zgłasza wyjątku (Perl lub C), należy jawnie sprawdzić wynik close()i zgłosić wyjątek, jeśli się nie powiedzie.

Powyższa sugestia, aby po prostu wsypać plik do pamięci, manipulować nim i zapisać go do pliku, gwarantuje utworzenie plików o zerowej długości na pełnym systemie plików. Trzeba zawsze używać FileUtils.mvdo poruszania się w pełni napisany plik tymczasowy na miejscu.

Ostatnią kwestią jest umieszczenie pliku tymczasowego. Jeśli otworzysz plik w / tmp, musisz wziąć pod uwagę kilka problemów:

  • Jeśli / tmp jest zamontowany w innym systemie plików, możesz uruchomić / tmp bez miejsca przed wypisaniem pliku, który w innym przypadku byłby możliwy do wdrożenia w miejscu docelowym starego pliku.

  • Prawdopodobnie ważniejsze jest to, że kiedy spróbujesz mvprzenieść plik na urządzenie, zostaniesz przekonwertowany na cpzachowanie. Stary plik zostanie otwarty, stary i-węzeł plików zostanie zachowany i ponownie otwarty, a zawartość pliku zostanie skopiowana. Najprawdopodobniej to nie jest to, czego chcesz i możesz napotkać błąd „plik tekstowy zajęty”, jeśli spróbujesz edytować zawartość uruchomionego pliku. To również przeczy celowi używania mvpoleceń systemu plików i możesz uruchomić docelowy system plików bez miejsca, mając tylko częściowo zapisany plik.

    Nie ma to również nic wspólnego z implementacją Rubiego. System mvi cppolecenia zachowują się podobnie.

Bardziej preferowane jest otwarcie pliku tymczasowego w tym samym katalogu, w którym znajduje się stary plik. Gwarantuje to, że nie będzie problemów z przenoszeniem między urządzeniami. mvSama nigdy nie uda, i zawsze należy uzyskać pełną i untruncated pliku. Podczas zapisywania pliku tymczasowego należy napotkać wszelkie awarie, takie jak brak miejsca na urządzeniu, błędy uprawnień itp.

Jedyne wady podejścia do tworzenia pliku tymczasowego w katalogu docelowym to:

  • Czasami możesz nie być w stanie otworzyć tam pliku tymczasowego, na przykład, jeśli próbujesz „edytować” plik w / proc. Z tego powodu możesz chcieć wycofać się i spróbować / tmp, jeśli otwarcie pliku w katalogu docelowym nie powiedzie się.
  • Musisz mieć wystarczająco dużo miejsca na partycji docelowej, aby pomieścić zarówno cały stary plik, jak i nowy plik. Jednakże, jeśli nie masz wystarczającej ilości miejsca na przechowywanie obu kopii, prawdopodobnie brakuje Ci miejsca na dysku, a rzeczywiste ryzyko zapisania obciętego pliku jest znacznie wyższe, więc uważam, że jest to bardzo słaby kompromis poza wyjątkowo wąskimi (i dobrze -monitored) skrajne przypadki.

Oto kod, który implementuje pełny algorytm (kod systemu Windows jest nieprzetestowany i niedokończony):

#!/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")

A tutaj jest nieco ściślejsza wersja, która nie przejmuje się wszystkimi możliwymi przypadkami skrajnymi (jeśli używasz Uniksa i nie obchodzi Cię pisanie do / 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")

Naprawdę prosty przypadek użycia, gdy nie dbasz o uprawnienia systemu plików (albo nie pracujesz jako root, albo pracujesz jako root i plik należy do administratora):

#!/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 : Powinno to być używane zamiast zaakceptowanej odpowiedzi jako minimum we wszystkich przypadkach, aby mieć pewność, że aktualizacja jest niepodzielna, a współbieżni czytelnicy nie zobaczą obciętych plików. Jak wspomniałem powyżej, utworzenie pliku Temp w tym samym katalogu co edytowany plik jest tutaj ważne, aby uniknąć tłumaczenia operacji mv na różnych urządzeniach na operacje cp, jeśli / tmp jest zamontowany na innym urządzeniu. Wywołanie fdatasync to dodatkowa warstwa paranoi, ale spowoduje to spadek wydajności, więc pominąłem to w tym przykładzie, ponieważ nie jest powszechnie praktykowane.


Zamiast otwierać plik tymczasowy w katalogu, w którym się znajdujesz, w rzeczywistości automatycznie utworzy go w katalogu danych aplikacji (w każdym razie w systemie Windows) iz ich poziomu możesz zrobić plik.unlink, aby go usunąć ..
13aal

3
Naprawdę doceniam dodatkową myśl, która została w to włożona. Dla początkujących bardzo interesujące jest zobaczenie schematów myślowych doświadczonych programistów, którzy nie tylko potrafią odpowiedzieć na oryginalne pytanie, ale także skomentować szerszy kontekst tego, co tak naprawdę oznacza oryginalne pytanie.
ramijames

Programowanie to nie tylko naprawianie bezpośredniego problemu, ale także myślenie z wyprzedzeniem, aby uniknąć innych problemów, które czekają. Nic tak nie irytuje starszego programisty, jak napotkanie kodu, który zamalował algorytm w róg, wymuszając niezręczny bałagan, podczas gdy niewielka wcześniejsza korekta spowodowałaby niezły przepływ. Często analiza celu może zająć godziny lub dni, a następnie kilka wierszy zastępuje stronę ze starym kodem. To jak gra w szachy z danymi i czasami z systemem.
Tin Man

11

Tak naprawdę nie ma sposobu na edycję plików w miejscu. To, co zwykle robisz, kiedy możesz sobie z tym poradzić (tj. Jeśli pliki nie są zbyt duże), to wczytujesz plik do pamięci ( File.read), wykonujesz podstawienia na odczytanym ciągu ( String#gsub), a następnie zapisujesz zmieniony ciąg z powrotem do plik ( File.open, File#write).

Jeśli pliki są wystarczająco duże, aby było to niewykonalne, to co musisz zrobić, to czytać plik w fragmentach (jeśli wzór, który chcesz zastąpić, nie obejmuje wielu linii, to jeden fragment zwykle oznacza jedną linię - możesz użyć File.foreachdo czytaj plik wiersz po wierszu), a dla każdej porcji wykonaj na nim podstawienie i dołącz go do pliku tymczasowego. Kiedy skończysz iterować plik źródłowy, zamknij go i użyj, FileUtils.mvaby zastąpić go plikiem tymczasowym.


1
Podoba mi się podejście do przesyłania strumieniowego. Jednocześnie mamy do czynienia z dużymi plikami, więc zwykle nie mamy miejsca w pamięci RAM, aby odczytać cały plik
Shane

Dlaczego„ siorbanie ”pliku nie jest dobrą praktyką? ” Może być przydatna w związku z tym.
Tin Man

9

Innym podejściem jest użycie edycji w miejscu w Rubim (nie z wiersza poleceń):

#!/usr/bin/ruby

def inplace_edit(file, bak, &block)
    old_stdout = $stdout
    argf = ARGF.clone

    argf.argv.replace [file]
    argf.inplace_mode = bak
    argf.each_line do |line|
        yield line
    end
    argf.close

    $stdout = old_stdout
end

inplace_edit 'test.txt', '.bak' do |line|
    line = line.gsub(/search1/,"replace1")
    line = line.gsub(/search2/,"replace2")
    print line unless line.match(/something/)
end

Jeśli nie chcesz tworzyć kopii zapasowej, zmień '.bak'na ''.


1
Byłoby to lepsze niż próba slurp ( read) pliku. Jest skalowalny i powinien być bardzo szybki.
Tin Man

Gdzieś jest błąd powodujący, że Ruby 2.3.0p0 w systemie Windows nie działa z odmową uprawnień, jeśli na tym samym pliku pracuje kilka kolejnych bloków inplace_edit. Aby odtworzyć testy podzielonego wyszukiwania1 i wyszukiwania2 na 2 bloki. Nie zamykasz całkowicie?
mlt

Spodziewałbym się problemów z wieloma edycjami pliku tekstowego występującymi jednocześnie. Jeśli nic innego, możesz dostać źle zniekształcony plik tekstowy.
Tin Man

7

To działa dla mnie:

filename = "foo"
text = File.read(filename) 
content = text.gsub(/search_regexp/, "replacestring")
File.open(filename, "w") { |file| file << content }

6

Oto rozwiązanie umożliwiające znajdowanie / zastępowanie we wszystkich plikach w danym katalogu. Zasadniczo wziąłem odpowiedź udzieloną przez sepp2k i rozszerzyłem ją.

# 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

4
require 'trollop'

opts = Trollop::options do
  opt :output, "Output file", :type => String
  opt :input, "Input file", :type => String
  opt :ss, "String to search", :type => String
  opt :rs, "String to replace", :type => String
end

text = File.read(opts.input)
text.gsub!(opts.ss, opts.rs)
File.open(opts.output, 'w') { |f| f.write(text) }

2
Bardziej pomoże, jeśli podasz wyjaśnienie, dlaczego jest to preferowane rozwiązanie i wyjaśnisz, jak to działa. Chcemy uczyć, a nie tylko dostarczać kod.
Tin Man

trollop został przemianowany na optymistę github.com/manageiq/optimist . Jest to po prostu parser opcji CLI, który nie jest naprawdę wymagany do odpowiedzi na pytanie.
noraj

1

Jeśli musisz wykonać podstawienia w granicach linii, użycie ruby -pi -enie zadziała, ponieważ pprzetwarza jedną linię na raz. Zamiast tego zalecam następujące czynności, chociaż może się to nie udać w przypadku pliku o wielu GB:

ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))"

Poszukuje białych znaków (potencjalnie zawierających nowe linie), po których następuje cudzysłów, w którym to przypadku pozbywa się białych znaków. To %q(')tylko fantazyjny sposób cytowania znaku cudzysłowu.


1

Tutaj alternatywa dla one linera od jima, tym razem w scenariuszu

ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))}

Zapisz go w skrypcie, np. Replace.rb

Zaczynasz w wierszu poleceń z

replace.rb *.txt <string_to_replace> <replacement>

* .txt można zastąpić innym zaznaczeniem lub niektórymi nazwami plików lub ścieżkami

podzielony, aby móc wyjaśnić, co się dzieje, ale nadal jest wykonywalny

# ARGV is an array of the arguments passed to the script.
ARGV[0..-3].each do |f| # enumerate the arguments of this script from the first to the last (-1) minus 2
  File.write(f,  # open the argument (= filename) for writing
    File.read(f) # open the argument (= filename) for reading
    .gsub(ARGV[-2],ARGV[-1])) # and replace all occurances of the beforelast with the last argument (string)
end

EDYTUJ: jeśli chcesz użyćwyrażenia regularnego użyj tego Oczywiście jest to tylko do obsługi stosunkowo małych plików tekstowych, bez gigabajtowych potworów

ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}

Ten kod nie zadziała. Proponuję przetestować go przed wysłaniem, a następnie skopiować i wkleić działający kod.
Tin Man

@theTinMan Zawsze testuję przed publikacją, jeśli to możliwe. Przetestowałem to i działa, zarówno ta krótka, jak i komentowana wersja. Jak myślisz, dlaczego nie?
Piotr

jeśli masz na myśli użycie wyrażenia regularnego, zobacz moją edycję, również przetestowaną:>)
peter
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.