使用 Ruby 逐行读取、编辑和写入文本文件

Posted

技术标签:

【中文标题】使用 Ruby 逐行读取、编辑和写入文本文件【英文标题】:Read, edit, and write a text file line-wise using Ruby 【发布时间】:2011-05-22 19:06:49 【问题描述】:

有没有在 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 文件中的一些行。

【问题讨论】:

如果要读取然后覆盖的代码在过程中途失败,请考虑结果:您将面临破坏文件的风险。 好的,作为后续问题:从命令行,您可以这样做:ruby -pe "gsub(/blah/,'newstuff')" whatev.txt。这就是我想做的,但我不想在命令行上那样做,我想把它放在更大的东西里。谁能告诉我,在内部,该命令在做什么会产生逐行编辑文件的错觉?它是写入临时文件还是使用数组?因为它似乎可以相当快地处理相当大的文件,比这里提供的建议要快。 这是个好问题。你能把它变成一个新问题吗?这使得其他人更容易看到它并回答它。另外,如果这个问题的回答令您满意,您能接受这个答案吗?谢谢! 虽然逐行读取文件并写入新文件似乎效率低下,但实际上the speed is equal-to or better-than trying to read a huge file into memory,修改它并将其写回。这样做是一种公认​​的编程实践,而且,不,一旦考虑到速度、内存要求和数据安全性,确实没有更好的解决方案。 【参考方案1】:

一般来说,无法在文件中间进行任意编辑。这不是Ruby的不足。这是文件系统的一个限制:大多数文件系统都可以轻松高效地在末尾增大或缩小文件,但不能在开头或中间进行。因此,除非它的大小保持不变,否则您将无法就地重写一行。

修改一堆行有两种通用模型。如果文件不是太大,只需将其全部读入内存,修改它,然后将其写回。例如,在文件每一行的开头添加“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 块确保临时文件被立即清理,而无需等待它被垃圾收集。

【讨论】:

+1 修改文件时最好保守一点,尤其是大文件。 你即将关闭 temp_file,为什么要倒带? @hihell,BookOfGreg 的编辑添加了倒带;他的评论是:“FileUtils.mv 将写入一个空白文件,除非临时文件被倒带。最好的做法是确保临时文件在使用后关闭并取消链接。” 第二种情况下文件的创建日期会发生什么? FileUtils.mv 会导致我们得到一个看起来好像刚刚创建的文件吗?如果是这样,这两种情况之间的差异很大(因为第一种情况只保留文件创建日期)。 @Matt 我从来没有想过这种技术对创建日期的影响,但很明显你是对的。【参考方案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

执行后,在脚本的末尾,您现在拥有以下内容,而不是我假设的内容。

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

【讨论】:

以上是关于使用 Ruby 逐行读取、编辑和写入文本文件的主要内容,如果未能解决你的问题,请参考以下文章

关于C语言中文本文件的逐行读取的实现

Objective C - 创建文本文件以在 Cocoa 中逐行读取和写入

在 VBA 中逐行读取/解析文本文件

labview如何读取文本文档中某一行的字符串

VB6.0中如何实现逐行读入文本文件?

Ruby脚本逐行读取文件并使用puts执行if语句?