Подтвердить что ты не робот

Чтение, редактирование и запись текстового файла с использованием Ruby

Есть ли хороший способ читать, редактировать и записывать файлы в Ruby?

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

Что-то вроде:

myfile = File.open("path/to/file.txt", "r+")

myfile.each do |line|
    myfile.replace_puts('blah') if line =~ /myregex/
end

myfile.close

Где replace_puts будет писать поверх текущей строки, а не (над) записывать следующую строку, как она сейчас делает, потому что указатель находится в конце строки (после разделителя).

Итак, каждая строка, соответствующая /myregex/, будет заменена на "blah". Очевидно, что я имею в виду немного более активное участие, чем в отношении обработки, и будет выполняться в одной строке, но идея одна и та же: я хочу читать файл за строкой и редактировать определенные строки и напишите, когда закончите.

Может быть, есть способ просто сказать "перемотать назад сразу после последнего разделителя"? Или какой-либо способ использования each_with_index и написать через номер индекса строки? Однако я не мог найти ничего подобного.

Лучшее решение, которое я имею до сих пор, - это прочитать строки по строкам, записать их в новый (временный) файл по линии (возможно, отредактированный), а затем перезаписать старый файл с помощью нового файла temp и удалить. Опять же, я чувствую, что должен быть лучший способ - я не думаю, что мне нужно будет создать новый 1gig файл, чтобы редактировать некоторые строки в существующем 1GB файле.

4b9b3361

Ответ 1

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

Существуют две общие модели для изменения последовательности строк. Если файл не слишком большой, просто прочитайте его все в памяти, измените его и запишите. Например, добавление "Kilroy было здесь" в начало каждой строки файла:

path = '/tmp/foo'
lines = IO.readlines(path).map do |line|
  'Kilroy was here ' + line
end
File.open(path, 'w') do |file|
  file.puts lines
end

Хотя это просто, этот метод имеет опасность: если программа прервана во время записи файла, вы потеряете часть или все это. Для хранения всего файла также необходимо использовать память. Если это одна из проблем, то вы можете предпочесть следующий метод.

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

require 'tempfile'
require 'fileutils'

path = '/tmp/foo'
temp_file = Tempfile.new('foo')
begin
  File.open(path, 'r') do |file|
    file.each_line do |line|
      temp_file.puts 'Kilroy was here ' + line
    end
  end
  temp_file.close
  FileUtils.mv(temp_file.path, path)
ensure
  temp_file.close
  temp_file.unlink
end

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

Предложение ensure не является строго необходимым: файл будет удален, когда экземпляр Tempfile будет собран мусором. Однако это может занять некоторое время. Блок ensure гарантирует, что tempfile будет очищен сразу же, не дожидаясь, когда он будет собран в мусор.

Ответ 2

Если вы хотите переписать файл по строкам, вам необходимо убедиться, что новая строка имеет ту же длину, что и исходная строка. Если новая строка длиннее, ее часть будет записана в следующей строке. Если новая строка короче, остальная часть старой строки остается там, где она есть. Решение tempfile действительно намного безопаснее. Но если вы готовы пойти на риск:

File.open('test.txt', 'r+') do |f|   
    old_pos = 0
    f.each do |line|
        f.pos = old_pos   # this is the 'rewind'
        f.print line.gsub('2010', '2011')
        old_pos = f.pos
    end
end

Если размер строки изменяется, это возможно:

File.open('test.txt', 'r+') do |f|   
    out = ""
    f.each do |line|
        out << line.gsub(/myregex/, 'blah') 
    end
    f.pos = 0                     
    f.print out
    f.truncate(f.pos)             
end

Ответ 3

На всякий случай, если вы используете Rails или Facets, иначе вы в противном случае зависете от Rails ActiveSupport, вы можете использовать расширение atomic_write для File:

File.atomic_write('path/file') do |file|
  file.write('your content')
end

За кулисами это создаст временный файл, который позже переместится на нужный путь, заботясь о закрытии файла для вас.

Он также клонирует права доступа к файлу существующего файла или, если его нет, текущего каталога.

Ответ 4

Вы можете писать в середине файла, но вы должны быть осторожны, чтобы сохранить длину строки, которую вы перезаписываете, в противном случае вы перезаписываете некоторые из следующих текстов. Я приводил пример здесь, используя File.seek, IO:: SEEK_CUR дает текущую позицию указателя файла, в конце строки, которая только что прочитана, +1 для символа CR в конце строки.

look_for     = "bbb"
replace_with = "xxxxx"

File.open(DATA, 'r+') do |file|
  file.each_line do |line|
    if (line[look_for])
      file.seek(-(line.length + 1), IO::SEEK_CUR)
      file.write line.gsub(look_for, replace_with)
    end
  end
end
__END__
aaabbb
bbbcccddd
dddeee
eee

После выполнения, в конце script теперь у вас есть следующее, а не то, что вы имели в виду, я предполагаю.

aaaxxxxx
bcccddd
dddeee
eee

Учитывая это, скорость, использующая этот метод, намного лучше, чем классический метод "читать и писать в новый файл". См. Эти тесты в файле с музыкальными данными размером 1,7 ГБ. Для классического подхода я использовал технику Уэйн. Тест выполняется с использованием метода .bmbm, так что кэширование файла не играет большой роли. Тесты выполняются с помощью MRI Ruby 2.3.0 в Windows 7. Строки были эффективно заменены, я проверил оба метода.

require 'benchmark'
require 'tempfile'
require 'fileutils'

look_for      = "Melissa Etheridge"
replace_with  = "Malissa Etheridge"
very_big_file = 'D:\Documents\muziekinfo\all.txt'.gsub('\\','/')

def replace_with file_path, look_for, replace_with
  File.open(file_path, 'r+') do |file|
    file.each_line do |line|
      if (line[look_for])
        file.seek(-(line.length + 1), IO::SEEK_CUR)
        file.write line.gsub(look_for, replace_with)
      end
    end
  end
end

def replace_with_classic path, look_for, replace_with
  temp_file = Tempfile.new('foo')
  File.foreach(path) do |line|
    if (line[look_for])
      temp_file.write line.gsub(look_for, replace_with)
    else
      temp_file.write line
    end
  end
  temp_file.close
  FileUtils.mv(temp_file.path, path)
ensure
  temp_file.close
  temp_file.unlink
end

Benchmark.bmbm do |x| 
  x.report("adapt          ") { 1.times {replace_with very_big_file, look_for, replace_with}}
  x.report("restore        ") { 1.times {replace_with very_big_file, replace_with, look_for}}
  x.report("classic adapt  ") { 1.times {replace_with_classic very_big_file, look_for, replace_with}}
  x.report("classic restore") { 1.times {replace_with_classic very_big_file, replace_with, look_for}}
end 

Что дало

Rehearsal ---------------------------------------------------
adapt             6.989000   0.811000   7.800000 (  7.800598)
restore           7.192000   0.562000   7.754000 (  7.774481)
classic adapt    14.320000   9.438000  23.758000 ( 32.507433)
classic restore  14.259000   9.469000  23.728000 ( 34.128093)
----------------------------------------- total: 63.040000sec

                      user     system      total        real
adapt             7.114000   0.718000   7.832000 (  8.639864)
restore           6.942000   0.858000   7.800000 (  8.117839)
classic adapt    14.430000   9.485000  23.915000 ( 32.195298)
classic restore  14.695000   9.360000  24.055000 ( 33.709054)

Таким образом, замена in_file была в 4 раза быстрее.