Як шукати текст у файлі за шаблоном і замінювати його заданим значенням


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

Чи ставить записувати зміну назад у файл? Я думав, що це просто надрукує вміст на консолі.
Дейн О'Коннор

Так, він друкує вміст на консолі.
sepp2k

7
Так, я не був впевнений, що ти цього хотів. Для запису використовуйте File.open (ім'я_файлу, "w") {| файл | file.puts output_of_gsub}
Макс Черняк

7
Мені довелося скористатися file.write: File.open (ім'я файлу, "w") {| файл | file.write (текст)}
Остен

3
Щоб написати файл, замініть "рядок" зFile.write(file_name, text.gsub(/regexp/, "replace")
щільним

106

Насправді, у Ruby є функція редагування на місці. Як і Perl, можна сказати

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

Це застосує код у подвійних лапках до всіх файлів у поточному каталозі, імена яких закінчуються на ".txt". Резервні копії відредагованих файлів будуть створені з розширенням ".bak" (я думаю, "foobar.txt.bak").

ПРИМІТКА. Схоже, це не працює для багаторядкових пошукових запитів. Для тих, хто повинен зробити це іншим менш симпатичним способом, із скриптом для обгортки навколо регулярного виразу.


1
Що за чорт pi.bak? Без цього я отримую помилку. -e: 1: in <main>': undefined method gsub 'for main: Object (NoMethodError)
Ninad

15
@NinadPachpute -iредагує на місці. .bak- це розширення, яке використовується для резервного файлу (необов'язково). -pщось подібне while gets; <script>; puts $_; end. ( $_є останнім рядком для читання, але ви можете призначити його за щось на зразок echo aa | ruby -p -e '$_.upcase!'.)
Lri,

1
Це краща відповідь, ніж прийнята відповідь, IMHO, якщо ви хочете змінити файл.
Колін К

6
Як я можу використовувати це всередині сценарію з рубіном ??
Саурах

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поведінку. Старий файл буде відкрито, старий файл inode буде збережено та відкрито, а вміст файлу буде скопійовано. Це, швидше за все, не те, що ви хочете, і ви можете зіткнутися з помилками "текстовий файл зайнятий", якщо спробуєте відредагувати вміст запущеного файлу. Це також перешкоджає використанню mvкоманд файлової системи, і ви можете запустити цільову файлову систему з простору лише лише частково записаним файлом.

    Це також не має нічого спільного з реалізацією Ruby. Система mvта cpкоманди поводяться аналогічно.

Більш переважно - це відкривати Tempfile у тому самому каталозі, що і старий файл. Це гарантує, що проблем із переміщенням між пристроями не виникне. mvСама ніколи не вийде з ладу, і ви завжди повинні отримувати повну та untruncated файл. Будь-які збої, такі як пристрій у просторі, помилки дозволу тощо, повинні виникати під час виходу темпфіла.

Єдиними недоліками підходу до створення темпфіла в каталозі призначення є:

  • Іноді ви, можливо, не зможете відкрити там Tempfile, наприклад, якщо ви намагаєтесь "редагувати" файл у / 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 : Це слід використовувати замість прийнятої відповіді як мінімум у всіх випадках для того, щоб переконатися, що оновлення є атомним, і одночасно читачі не побачать усічені файли. Як я вже згадував вище, створення Tempfile у тому ж каталозі, що і відредагований файл, тут важливий, щоб уникнути перехресних операцій mv-пристроїв, переведених на операції cp, якщо / tmp встановлено на іншому пристрої. Виклик fdatasync - це додатковий шар параної, але це спричинить хіт продуктивності, тому я опустив його з цього прикладу, оскільки це не зазвичай застосовується.


Замість того, щоб відкривати тимчасовий файл у каталозі, який ви знаходитесь, він фактично автоматично створить його у каталозі даних додатків (у будь-якому випадку в Windows), і з їхнього ви можете зробити file.unlink, щоб видалити його ..
13aal

3
Я дуже цінував додаткову думку, яка була вкладена в це. Як початківцю, дуже цікаво бачити схеми мислення досвідчених дияволів, які не можуть просто відповісти на початкове запитання, а й прокоментувати ширший контекст того, що насправді означає оригінальне питання.
ramijames

Програмування стосується не лише вирішення негайної проблеми, а й роздумування над способом, щоб уникнути інших проблем, які затримуються в очікуванні. Ніщо не дратує старшого розробника більше, ніж стикатися з кодом, який намалював алгоритм у кут, змушуючи незграбну помилку, коли незначне коригування раніше призвело б до хорошого потоку. Аналіз, щоб зрозуміти мету, може зайняти години або дні, а потім кілька сторінок заміняють сторінку старого коду. Це як гра в шахи проти даних і системи часом.
Олов'яний чоловік

11

Насправді не існує способу редагування файлів на місці. Що ви зазвичай робите, коли ви можете піти з ним (тобто, якщо файли не надто великі), це ви читаєте файл у пам'яті ( File.read), виконуєте свої заміни на рядку читання ( String#gsub), а потім записуєте змінений рядок назад у файл ( File.open, File#write).

Якщо файли достатньо великі, щоб це було нездійсненно, то, що вам потрібно зробити, - це прочитати файл шматками (якщо шаблон, який ви хочете замінити, не може охоплювати декілька рядків, то один фрагмент зазвичай означає один рядок - ви можете використовувати File.foreachдля читайте файл рядок за рядком), і для кожного фрагменту виконайте заміну на ньому та додайте його до тимчасового файлу. Закінчивши ітерацію над вихідним файлом, ви закриєте його та використовуєте FileUtils.mvдля перезапис його тимчасовим файлом.


1
Мені подобається потоковий підхід. Ми маємо справу з великими файлами одночасно, тому у нас зазвичай не залишається місця в оперативній пам’яті, щоб прочитати весь файл
Shane


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'до ''.


1
Це було б краще, ніж намагатися виконувати readфайл slurp ( ). Це масштабується і має бути дуже швидким.
Олов'яна людина

Десь виникла помилка, через яку 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) }

2
Це допомагає більше, якщо ви надасте пояснення, чому це найкраще рішення та поясніть, як воно працює. Ми хочемо вчити, а не просто надавати код.
Олов'яний чоловік

trollop був перейменований на оптиміст github.com/manageiq/optimist . Крім того, це просто розбір варіантів CLI, не дуже потрібний для відповіді на питання.
noraj

1

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

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

Шукає білий простір (можливо, включаючи нові рядки), слідуючи цитатою, і в цьому випадку він позбавляється пробілу. Це %q(')просто фантазійний спосіб цитування символу цитати.


1

Тут альтернатива одному вкладиші від Jim, цього разу в сценарії

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

Збережіть його у сценарії, наприклад, замініть.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

EDIT: якщо ви хочете використовувати звичайний вираз, використовуйте це замість цього. Очевидно, це лише для обробки відносно невеликих текстових файлів, без гігабайтних монстрів

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

Цей код не працюватиме. Я б запропонував протестувати його перед публікацією, після чого скопіюйте та вставте робочий код.
Олов'яний чоловік

@theTinMan Я завжди тестую перед публікацією, якщо можливо. Я перевірив це, і це працює, як коротка, так і коментована версія. Чому ти вважаєш, що це не буде?
пітер

якщо ви маєте на увазі використання регулярного виразу, дивіться мою редагування, також перевірену:>)
peter
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.