Как найти в тексте файла шаблон и заменить его заданным значением

117

Я ищу сценарий для поиска в файле (или списке файлов) шаблона и, если он найден, заменяю этот шаблон заданным значением.

Мысли?

Дэйн О'Коннор
источник
1
В ответах ниже имейте в виду, что любые рекомендации по использованию File.readдолжны быть дополнены информацией из stackoverflow.com/a/25189286/128421 о том, почему прихлебывать большие файлы - это плохо. Также вместо File.open(filename, "w") { |file| file << content }вариаций используйте File.write(filename, content).
Железный Человек

Ответы:

190

Отказ от ответственности: этот подход является наивной иллюстрацией возможностей Ruby, а не производственным решением для замены строк в файлах. Он подвержен различным сценариям сбоев, таким как потеря данных в случае сбоя, прерывания или переполнения диска. Этот код не годится ни для чего, кроме быстрого одноразового скрипта, в котором создается резервная копия всех данных. По этой причине НЕ копируйте этот код в свои программы.

Вот быстрый способ сделать это.

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
Макс Черняк
источник
Записывает ли put изменения обратно в файл? Я думал, что это просто напечатает контент на консоли.
Дэйн О'Коннор,
Да, он выводит содержимое на консоль.
sepp2k
7
Да, я не был уверен, что ты этого хочешь. Для записи используйте File.open (имя_файла, "w") {| file | file.puts output_of_gsub}
Макс Черняк
7
Мне пришлось использовать file.write: File.open (имя_файла, "w") {| file | file.write (text)}
Остин
3
Чтобы записать файл, замените строку File.write(file_name, text.gsub(/regexp/, "replace")
tight
106

На самом деле в Ruby есть функция редактирования на месте. Как и Perl, вы можете сказать

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

Это применит код в двойных кавычках ко всем файлам в текущем каталоге, имена которых заканчиваются на «.txt». Резервные копии редактируемых файлов будут создаваться с расширением «.bak» (думаю, «foobar.txt.bak»).

ПРИМЕЧАНИЕ: похоже, это не работает для многострочного поиска. Для них вы должны сделать это другим, менее красивым способом, с помощью сценария-оболочки вокруг регулярного выражения.

Джим Кейн
источник
1
Что, черт возьми, такое пи.бак? Без этого я получаю ошибку. -e: 1: in <main>': undefined method gsub 'для main: Object (NoMethodError)
Ninad
15
@NinadPachpute -iправки на месте. .bak- расширение, используемое для файла резервной копии (необязательно). -pэто что-то вроде while gets; <script>; puts $_; end. ( $_это последняя прочитанная строка, но вы можете назначить ей что-то вроде echo aa | ruby -p -e '$_.upcase!'.)
Пт,
1
Это лучший ответ, чем принятый ответ, IMHO, если вы хотите изменить файл.
Colin K
6
Как я могу использовать это внутри скрипта ruby ​​??
Саураб
1
Есть много причин, по которым это может пойти не так, поэтому тщательно протестируйте его, прежде чем пытаться применить его к важному файлу.
Железный Человек
49

Имейте в виду, что при этом в файловой системе может не хватить места, и вы можете создать файл нулевой длины. Это катастрофа, если вы делаете что-то вроде записи файлов / etc / passwd в рамках управления конфигурацией системы.

Обратите внимание, что редактирование файла на месте, как в принятом ответе, всегда будет усекать файл и последовательно записывать новый файл. Всегда будет состояние гонки, при котором одновременные читатели увидят усеченный файл. Если процесс прерывается по какой-либо причине (ctrl-c, убийца OOM, сбой системы, отключение питания и т. Д.) Во время записи, то усеченный файл также останется, что может иметь катастрофические последствия. Это тот сценарий потери данных, который разработчики ДОЛЖНЫ учитывать, потому что это произойдет. По этой причине я думаю, что принятый ответ, скорее всего, не должен быть принятым ответом. Как минимум напишите во временный файл и переместите / переименуйте файл на место, как «простое» решение в конце этого ответа.

Вам необходимо использовать алгоритм, который:

  1. Читает старый файл и записывает в новый файл. (Вам нужно быть осторожным, чтобы целые файлы не попали в память).

  2. Явно закрывает новый временный файл, в котором вы можете вызвать исключение, потому что файловые буферы не могут быть записаны на диск из-за отсутствия места. (Поймайте это и очистите временный файл, если хотите, но на этом этапе вам нужно что-то перебросить заново или довольно сильно выйти из строя.

  3. Исправляет права доступа и режимы для нового файла.

  4. Переименовывает новый файл и вставляет его на место.

С файловыми системами ext3 вам гарантируется, что метаданные, записываемые для перемещения файла на место, не будут переупорядочены файловой системой и записаны до того, как будут записаны буферы данных для нового файла, поэтому это должно быть либо успешно, либо неуспешно. Файловая система ext4 также была исправлена ​​для поддержки такого поведения. Если вы очень параноик, вам следует вызвать fdatasync()системный вызов в качестве шага 3.5 перед перемещением файла на место.

Независимо от языка это лучшая практика. В языках, где вызов close()не вызывает исключения (Perl или C), вы должны явно проверить возврат close()и выбросить исключение в случае сбоя.

Приведенное выше предложение просто поместить файл в память, манипулировать им и записать его в файл гарантированно приведет к созданию файлов нулевой длины в полной файловой системе. Вам необходимо всегда использовать FileUtils.mvдля перемещения полностью записанного временного файла на место.

Последнее соображение - это размещение временного файла. Если вы открываете файл в / tmp, вы должны учитывать несколько проблем:

  • Если / tmp смонтирован в другой файловой системе, вы можете запустить / tmp из-за отсутствия свободного места, прежде чем записать файл, который в противном случае можно было бы развернуть в место назначения старого файла.

  • Вероятно, что еще более важно, когда вы пытаетесь подключить mvфайл через устройство, вы прозрачно конвертируетесь в cpповедение. Старый файл будет открыт, индексный дескриптор старых файлов будет сохранен и повторно открыт, а содержимое файла будет скопировано. Скорее всего, это не то, что вам нужно, и вы можете столкнуться с ошибкой «текстовый файл занят», если попытаетесь отредактировать содержимое работающего файла. Это также противоречит цели использования mvкоманд файловой системы, и вы можете запустить целевую файловую систему из-за недостатка места только с частично записанным файлом.

    Это также не имеет ничего общего с реализацией Ruby. Система mvи cpкоманды ведут себя аналогично.

Более предпочтительно открыть временный файл в том же каталоге, что и старый файл. Это гарантирует, что не возникнет проблем с перемещением между устройствами. Сам по mvсебе никогда не должен выходить из строя, и вы всегда должны получать полный и не усеченный файл. Любые сбои, такие как нехватка места на устройстве, ошибки разрешений и т. Д., Должны возникать во время записи временного файла.

Единственными недостатками подхода к созданию временного файла в целевом каталоге являются:

  • Иногда вы не сможете открыть там временный файл, например, если вы пытаетесь «отредактировать» файл в / proc. По этой причине вы можете вернуться и попробовать / tmp, если открытие файла в целевом каталоге не удается.
  • У вас должно быть достаточно места в целевом разделе, чтобы вместить как старый, так и новый файл полностью. Однако, если у вас недостаточно места для хранения обеих копий, тогда у вас, вероятно, не хватает места на диске, и фактический риск записи усеченного файла намного выше, поэтому я бы сказал, что это очень плохой компромисс за пределами некоторых чрезвычайно узких (и хорошо контролируемые) крайние случаи.

Вот код, реализующий полный алгоритм (код Windows непроверенный и незаконченный):

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

А вот немного более плотная версия, которая не заботится обо всех возможных крайних случаях (если вы используете Unix и не заботитесь о записи в / 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")

Действительно простой вариант использования, когда вам не важны разрешения файловой системы (либо вы работаете не как root, либо вы работаете как root, а файл принадлежит root):

#!/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 : как минимум, это следует использовать вместо принятого ответа во всех случаях, чтобы гарантировать, что обновление является атомарным и одновременные читатели не увидят усеченные файлы. Как я упоминал выше, создание временного файла в том же каталоге, что и отредактированный файл, важно здесь, чтобы избежать преобразования mv-операций между устройствами в операции cp, если / tmp смонтирован на другом устройстве. Вызов fdatasync - это дополнительный уровень паранойи, но он приведет к снижению производительности, поэтому я пропустил его в этом примере, поскольку он обычно не практикуется.

Ламонт
источник
Вместо того, чтобы открывать временный файл в каталоге, в котором вы находитесь, он фактически автоматически создает его в каталоге данных приложения (в любом случае в Windows), и из них вы можете удалить файл file.unlink, чтобы удалить его ..
13aal
3
Я действительно оценил дополнительные мысли, которые были вложены в это. Новичку очень интересно наблюдать за образцами мышления опытных разработчиков, которые могут не только ответить на исходный вопрос, но и прокомментировать более широкий контекст того, что на самом деле означает исходный вопрос.
ramijames 02
Программирование - это не только решение сиюминутной проблемы, но и умение думать на будущее, чтобы избежать других проблем, подстерегающих. Ничто так не раздражает старшего разработчика, как столкновение с кодом, который загнал алгоритм в угол, создавая неудобную путаницу, когда небольшая корректировка ранее привела бы к хорошему потоку. Часто для понимания цели могут потребоваться часы или дни, а затем несколько строк заменяют страницу старого кода. Иногда это похоже на игру в шахматы с данными и системой.
Железный Человек
11

На самом деле нет способа редактировать файлы на месте. Что вы обычно делаете, когда это может сойти с рук (например, если файлы не слишком большие), вы читаете файл в memory ( File.read), выполняете свои замены в строке чтения ( String#gsub), а затем записываете измененную строку обратно в файл ( File.open, File#write).

Если файлы достаточно велики, чтобы это было невозможно, вам нужно прочитать файл по частям (если шаблон, который вы хотите заменить, не будет охватывать несколько строк, то один фрагмент обычно означает одну строку - вы можете использовать File.foreachдля читать файл построчно), и для каждого фрагмента выполнить замену в нем и добавить его во временный файл. Когда вы закончите перебирать исходный файл, вы закрываете его и используете FileUtils.mvдля перезаписи временным файлом.

sepp2k
источник
1
Мне нравится потоковый подход. Мы работаем с большими файлами одновременно, поэтому обычно у нас нет места в ОЗУ для чтения всего файла
Шейн
« Почему« прихлебывать »файл не является хорошей практикой? » Может быть полезно прочитать в связи с этим.
Железный Человек
9

Другой подход - использовать редактирование на месте внутри Ruby (не из командной строки):

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

Если вы не хотите создавать резервную копию, измените '.bak'на ''.

DavidG
источник
1
Это было бы лучше, чем пытаться использовать slurp ( read) для файла. Он масштабируемый и должен быть очень быстрым.
The Tin Man
Где-то есть ошибка, приводящая к сбою Ruby 2.3.0p0 в Windows с отказом в разрешении, если с одним файлом работают несколько последовательных блоков inplace_edit. Воспроизвести разбиение тестов search1 и search2 на 2 блока. Не закрывается полностью?
mlt
Я бы ожидал, что проблемы с одновременным редактированием нескольких текстовых файлов будут. Если уж на то пошло, вы можете получить сильно искаженный текстовый файл.
Железный Человек
7

Это работает для меня:

filename = "foo"
text = File.read(filename) 
content = text.gsub(/search_regexp/, "replacestring")
File.open(filename, "w") { |file| file << content }
Ален Бовуа
источник
6

Вот решение для поиска / замены во всех файлах данного каталога. В основном я взял ответ, предоставленный sepp2k, и расширил его.

# 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) }
Ninad
источник
2
Будет лучше, если вы объясните, почему это предпочтительное решение, и объясните, как оно работает. Мы хотим обучать, а не просто предоставлять код.
Железный Человек
trollop был переименован в optimist github.com/manageiq/optimist . Кроме того, это просто синтаксический анализатор параметров интерфейса командной строки, который на самом деле не требуется для ответа на вопрос.
noraj
1

Если вам нужно выполнить замену через границы строк, то использование ruby -pi -eне будет работать, потому что pобрабатывается одна строка за раз. Вместо этого я рекомендую следующее, хотя это может привести к сбою с файлом размером в несколько ГБ:

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

Ищет пробелы (потенциально включая новые строки), за которыми следует кавычка, и в этом случае он избавляется от пробелов. Это %q(')просто причудливый способ цитирования символа кавычки.

Дэн Кон
источник
1

Вот альтернатива одному вкладышу от Джима, на этот раз в скрипте

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

Сохраните его в скрипте, например replace.rb

Вы начинаете в командной строке с

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

* .txt можно заменить другим выбором или некоторыми именами файлов или путями

разбит, чтобы я мог объяснить, что происходит, но все еще исполняемый

# 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

РЕДАКТИРОВАТЬ: если вы хотите использовать регулярное выражение, используйте это вместо этого Очевидно, это только для обработки относительно небольших текстовых файлов, без монстров Gigabyte

ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}
Питер
источник
Этот код работать не будет. Я предлагаю протестировать его перед публикацией, а затем скопировать и вставить рабочий код.
Железный Человек
@theTinMan Я всегда тестирую перед публикацией, если это возможно. Я проверил это, и он работает, как в версии с комментариями. Как вы думаете, почему нет?
питер
если вы имеете в виду , используя регулярное выражение , видеть мое редактирование, также протестировали:>)
Питер