如何在文件文本中搜索模式并将其替换为给定值


117

我正在寻找一个脚本来搜索文件(或文件列表)中的模式,如果找到,请用给定值替换该模式。

有什么想法吗?


1
在下面的答案中,请注意,使用任何建议都File.read需要根据stackoverflow.com/a/25189286/128421中的信息进行调整, 以解决为什么大文件打包不好的原因。另外,请File.open(filename, "w") { |file| file << content }使用代替变化File.write(filename, content)
Tin Man

Answers:


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(file_name,“ w”){| file | file.puts output_of_gsub}
Max Chernyak

7
我必须使用file.write:File.open(file_name,“ w”){| file | file.write(文字)}
奥斯汀

3
要写入文件,请将puts'行替换为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:在<main>': undefined method GSUB”的主要:对象(NoMethodError)
Ninad

15
@NinadPachpute -i编辑到位。.bak是用于备份文件的扩展名(可选)。-p就像while gets; <script>; puts $_; end。($_是最后读取的行,但是您可以为它分配类似的内容echo aa | ruby -p -e '$_.upcase!'。)
Lri

1
如果您要修改文件,这比公认的答案更好,恕我直言。
科林K

6
如何在ruby脚本中使用它?
沙拉卜

1
有很多方法可能会出错,因此请在对关键文件进行尝试之前进行彻底测试。
Tin Man

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的实现无关。系统mvcp命令的行为类似。

更可取的是在与旧文件相同的目录中打开一个Tempfile。这样可以确保不会出现跨设备移动的问题。在mv本身应该永远不会失败,你应该总是让一个完整的,未截断文件。在写出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:在所有情况下,都应使用TL; DR至少代替可接受的答案,以确保更新是原子的,并且并发阅读器不会看到截断的文件。如上文所述,在此处将Tempfile与已编辑文件创建在同一目录中非常重要,这样可以避免跨设备mv操作在/ tmp安装在其他设备上时转换为cp操作。调用fdatasync是一个额外的妄想症层,但是它将导致性能下降,因此在本示例中我将其省略,因为它并不常见。


而不是和你在它是实际上将自动创建一个应用程序中的数据目录(在Windows反正)的目录中打开一个临时文件从他们的你可以做一个file.unlink将其删除..
13aal

3
我非常感谢为此付出的额外思考。作为一个初学者,看到经验丰富的开发人员的思维模式非常有趣,他们不仅可以回答原始问题,而且可以在更大范围内评论原始问题的实际含义。
ramijames

编程不仅是要解决眼前的问题,还在于要先行思考,以免出现其他问题。当遇到稍稍的调整会产生不错的流程时,遇到高级代码开发人员便无济于事,因为遇到的代码会将算法绘制到一个角落,从而造成笨拙的纠缠。为了了解目标,通常可能需要花费数小时或数天的时间进行分析,然后用几行代码替换旧代码页面。有时就像是对数据和系统的国际象棋游戏。
锡人

11

确实没有一种就地编辑文件的方法。当可以使用它时(例如,如果文件不太大),通常会执行以下操作:将文件读入内存(File.read),对读取的字符串(String#gsub)进行替换,然后将更改后的字符串写回到文件(File.openFile#write)。

如果文件是足够大的,要成为不可行的,你需要做的,就是阅读以块的文件(如果模式要替换不会跨越多行,然后一个大块通常意味着一条线-您可以使用File.foreach来逐行读取文件),然后对每个块执行替换操作并将其附加到临时文件中。遍历源文件后,请关闭它并使用FileUtils.mv临时文件覆盖它。


1
我喜欢流媒体方法。我们同时处理大文件,因此我们通常没有RAM中的空间来读取整个文件
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()更好。它具有可扩展性,并且应该非常快。
Tin Man

如果在同一文件上有多个连续的inplace_edit块,则某处存在一个错误,该错误会导致Windows上的Ruby 2.3.0p0失败,并且权限被拒绝。要重现将search1和search2测试分为2个块​​。没有完全关闭?
mlt

我希望同时发生多个文本文件编辑问题。如果没有其他问题,您可能会得到文本文件损坏的情况。
Tin Man


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一次只能处理一行。取而代之的是,我建议以下内容,尽管它可能会因数GB的文件而失败:

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]))}

将其保存在脚本中,例如replace.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

编辑:如果要使用正则表达式,请改用此表达式。显然,这仅用于处理相对较小的文本文件,没有千兆字节的怪兽

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

此代码无效。我建议在发布之前进行测试,然后复制并粘贴工作代码。
锡人

@theTinMan如果可能的话,我总是在发布之前进行测试。我对此进行了测试,它的工作原理很简单,即简短的评论版。您为什么认为不会呢?
彼得

如果您的意思是使用正则表达式,请参阅我的编辑,也经过测试:>)
彼得
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.