web-dev-qa-db-ja.com

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」に置き換えられます。明らかに、私が念頭に置いているのは、処理に関してはそれよりも少し複雑で、1行で行われますが、考え方は同じです-ファイルを1行ずつ読み取り、特定の行を編集します終わったら書き出す。

「最後のセパレータの直後に巻き戻す」と言う方法があるかもしれません。または、each_with_indexを使用して、行インデックス番号で書き込む方法はありますか?しかし、私はそのようなものを見つけることができませんでした。

私がこれまでに持っている最善の解決策は、物事を行ごとに読み、それらを新しい(一時)ファイルに行ごとに(おそらく編集して)書き出し、古いファイルを新しい一時ファイルで上書きして削除することです。繰り返しますが、もっと良い方法があるはずです-既存の1GBファイルのいくつかの行を編集するためだけに新しい1gigファイルを作成する必要はないと思います。

49
Hsiu

一般的に、ファイルの途中で任意の編集を行う方法はありません。 Rubyの欠陥ではありません。これはファイルシステムの制限です。ほとんどのファイルシステムでは、ファイルを最初または途中ではなく、最後に簡単に効率よく拡大または縮小できます。そのため、サイズが同じでない限り、行を書き換えることはできません。

一連の行を変更するための2つの一般的なモデルがあります。ファイルが大きすぎない場合は、すべてをメモリに読み込んで変更し、書き戻します。たとえば、ファイルのすべての行の先頭に「Kilroy was here」を追加します。

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ブロックは、一時ファイルがガベージコレクションされるのを待たずに、すぐにクリーンアップされるようにします。

69
Wayne Conrad

ファイルを1行ずつ上書きする場合は、新しい行が元の行と同じ長さであることを確認する必要があります。新しい行が長い場合、その一部は次の行に上書きされます。新しい行が短い場合、古い行の残りの部分はそのままになります。一時ファイルのソリューションは本当に安全です。しかし、リスクを冒す意思がある場合:

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
8
steenslag

Railsまたは Facets を使用している場合、またはRailsの ActiveSupport に依存している場合にのみ、 atomic_writeFileの拡張:

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

舞台裏では、これにより一時ファイルが作成され、後で目的のパスに移動して、ファイルを閉じます。

さらに、既存のファイルまたは存在しない場合は現在のディレクトリのファイル許可を複製します。

2
Kostas Rousis

ファイルの途中に書き込むことはできますが、上書きする文字列の長さを同じにするように注意する必要があります。そうしないと、次のテキストの一部が上書きされます。ここで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

実行後、スクリプトの最後に次のようになりますが、私が思っていたものではありません。

aaaxxxxx
bcccddd
dddeee
eee

それを考慮すると、この手法を使用した速度は、従来の「新しいファイルの読み取りと書き込み」方式よりもはるかに優れています。 1.7 GBの音楽データを含むファイルでこれらのベンチマークを参照してください。古典的なアプローチでは、ウェインの手法を使用しました。ベンチマークは.bmbmメソッドを使用して行われるため、ファイルのキャッシュはあまり重要ではありません。テストは、MRI Ruby 2.3.0 on 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倍高速でした。

0
peter