使用Ruby按行读取,编辑和编写文本文件

时间:2010-12-09 10:51:48

标签: ruby file io

有没有一种在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并通过行索引号写?但是,我找不到任何类似的东西。

我到目前为止最好的解决方案是按顺序读取内容,将它们逐行写入新的(临时)文件(可能已编辑),然后用新的临时文件覆盖旧文件并删除。同样,我觉得应该有更好的方法 - 我认为我不应该创建一个新的1gig文件来编辑现有1GB文件中的某些行。

4 个答案:

答案 0 :(得分:67)

通常,无法在文件中间进行任意编辑。这不是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块确保临时清理临时文件,而不必等待它被垃圾回收。

答案 1 :(得分:8)

如果要逐行覆盖文件,则必须确保新行的长度与原始行的长度相同。如果新行更长,则其中一部分将写入下一行。如果新行较短,则旧行的其余部分将保持原样。 临时文件解决方案确实更安全。但如果你愿意承担风险:

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

答案 2 :(得分:1)

如果您使用的是Rails或Facets,或者您依赖Rails'ActiveSupport,则可以使用File扩展名File.atomic_write('path/file') do |file| file.write('your content') end

{{1}}

在幕后,这将创建一个临时文件,稍后它将移动到所需的路径,负责为您关闭文件。

它进一步克隆了现有文件的文件权限,如果没有,则克隆当前目录的文件权限。

答案 3 :(得分:0)

您可以在文件中间写入,但是您必须小心保持覆盖的字符串的长度,否则您将覆盖以下某些文本。我在这里使用File.seek给出一个例子,IO :: SEEK_CUR给出了文件指针的当前位置,在刚刚读取的行的末尾,+1代表该行末尾的CR字符。 / p>

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方法完成基准测试,以便缓存文件不会起到很大作用。在Windows 7上使用MRI Ruby 2.3.0进行测试。 字符串被有效替换,我检查了两种方法。

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倍。