パターンのファイルテキストを検索し、それを特定の値で置き換える方法


117

ファイル(またはファイルのリスト)でパターンを検索し、見つかった場合はそのパターンを指定された値に置き換えるスクリプトを探しています。

考え?


1
以下の回答では、使用File.readする推奨事項はすべて、大きなファイルを丸呑みすることが悪い理由として、stackoverflow.com / a / 25189286/128421の情報で調整する必要があることに注意してください 。また、File.open(filename, "w") { |file| file << content }バリエーションの代わりにを使用しますFile.write(filename, content)
ティンマン

回答:


190

免責事項: このアプローチはRubyの機能の単純な例であり、ファイル内の文字列を置き換えるための本番環境レベルのソリューションではありません。クラッシュ、割り込み、ディスクがいっぱいになった場合のデータ損失など、さまざまな障害シナリオが発生しやすくなります。このコードは、すべてのデータがバックアップされる簡単な1回限りのスクリプト以外には適していません。そのため、このコードをプログラムにコピーしないでください。

これを行う簡単な短い方法を次に示します。

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

putは変更をファイルに書き戻しますか?コンテンツをコンソールに出力するだけだと思いました。
デーン・オコナー

はい、コンテンツをコンソールに出力します。
sepp2k 2009

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(text)}
austen

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:in <main>': undefined method gsub 'for main:Object(NoMethodError)
Ninad

15
@NinadPachputeが-i編集を行います。.bakバックアップファイルに使用される拡張子です(オプション)。-pのようなものですwhile gets; <script>; puts $_; end。($_は最後の読み取り行ですが、に割り当てることができますecho aa | ruby -p -e '$_.upcase!'。)
Lri

1
ファイルの変更を検討している場合、これは受け入れられた回答であるIMHOよりも良い回答です。
Colin K

6
これをルビスクリプト内でどのように使用できますか?
Saurabh

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動作に変換されることです。古いファイルが開かれ、古いファイルのiノードが保持されて再度開かれ、ファイルの内容がコピーされます。これはおそらく望んでいることではなく、実行中のファイルの内容を編集しようとすると、「テキストファイルビジー」エラーが発生する可能性があります。これは、ファイルシステムmvコマンドを使用する目的にも反するものであり、部分的に書き込まれたファイルのみを使用して、宛先のファイルシステムをスペース不足で実行する可能性があります。

    これもRubyの実装とは関係ありません。システムmvcpコマンドは同様に動作します。

古いファイルと同じディレクトリでTempfileを開くのがより望ましいです。これにより、デバイス間の移動の問題が発生しなくなります。mv自身が失敗することはありません、あなたは常に完全で切り捨てられていないファイルを取得する必要があります。Tempfileの書き込み中に、デバイスの容量不足、権限エラーなどの障害が発生するはずです。

宛先ディレクトリに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")

ファイルシステムのアクセス許可を気にしない場合(ルートとして実行していないか、ルートとして実行していて、ファイルがルート所有である場合)の非常に単純なユースケース:

#!/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:更新がアトミックであり、同時リーダーが切り捨てられたファイルを表示しないようにするために、すべての場合において、最低でも承認された回答の代わりに使用する必要があります。上で述べたように、/ tmpが別のデバイスにマウントされている場合、クロスデバイスmv操作がcp操作に変換されないようにするには、編集済みファイルと同じディレクトリにTempfileを作成することが重要です。fdatasyncの呼び出しはパラノイアの追加レイヤーですが、パフォーマンスに影響を与えるため、一般的には実行されないため、この例では省略しました。


代わりに、あなたが実際に自動的に(とにかくWindows上)アプリのデータディレクトリ内に作成されます、それにしているディレクトリに一時ファイルを開くと、あなたがfile.unlinkを行うことができます彼らからそれを削除するには...
13aal

3
私はこれに入れられた余分な考えを本当に感謝しています。初心者として、元の質問に答えるだけでなく、元の質問が実際に何を意味するかというより大きなコンテキストにコメントすることもできる経験豊富な開発者の思考パターンを見るのは非常に興味深いです。
ramijames

プログラミングは、当面の問題を修正するだけでなく、他の待機中の問題を回避するための方法についても考えます。以前のマイナーな調整が素晴らしいフローをもたらしたとしても、アルゴリズムを隅々まで描いたコードに出会い、ぎこちないことを強いることほど、上級開発者を苛立たせるものは何もありません。多くの場合、目標を理解するために分析に数時間または数日かかることがあります。その後、数行で古いコードのページを置き換えます。それは時々、データとシステムに対するチェスのゲームのようなものです。
ティンマン

11

ファイルをその場で編集する方法は実際にはありません。ファイルを大きくしない場合(つまり、ファイルが大きすぎない場合)は通常、ファイルをメモリに読み込み(File.read)、読み取り文字列に置換を実行し()、String#gsub変更された文字列をファイル(File.openFile#write)。

ファイルがそれを実行不可能にするのに十分な大きさである場合、あなたがしなければならないことは、ファイルをチャンクで読み取ることです(置換するパターンが複数行にまたがらない場合、1つのチャンクは通常1行を意味します-を使用File.foreachして行ごとにファイルを読み取ります)。チャンクごとに置換を実行し、一時ファイルに追加します。ソースファイルの反復が完了したら、ソースファイルを閉じ、を使用FileUtils.mvして一時ファイルで上書きします。


1
ストリーミングのアプローチが好きです。大きなファイルを同時に処理するため、通常はRAM内にファイル全体を読み取るためのスペースがありません
Shane

ファイルを「丸呑み」することが良い習慣ではないのはなぜですか?」これに関連して読むと役立つかもしれません。
Tin Man、

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、ファイルを丸呑みにする()よりも優れています。スケーラブルで、非常に高速でなければなりません。
Tin Man

同じファイルで複数の連続するinplace_editブロックが動作している場合、Windows上のRuby 2.3.0p0が権限拒否で失敗する原因となるバグがどこかにあります。分割されたsearch1およびsearch2テストを2つのブロックに再現します。完全に閉じていませんか?
mlt

テキストファイルの複数の編集が同時に発生するという問題が発生するはずです。他に何もない場合、ひどく壊れたテキストファイルを取得できます。
ティンマン

7

これは私にとってはうまくいきます:

filename = "foo"
text = File.read(filename) 
content = text.gsub(/search_regexp/, "replacestring")
File.open(filename, "w") { |file| file << content }

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
これが望ましいソリューションである理由と、それがどのように機能するかを説明すると、さらに役立ちます。コードを提供するだけでなく、教育したいと考えています。
Tin Man、

trollopはoptimist github.com/manageiq/optimistに名前が変更されました。また、質問に答える必要のないCLIオプションパーサーにすぎません。
noraj

1

行の境界を越えて置換を行う必要がある場合、は一度に1行ずつ処理するruby -pi -eため、は機能しませんp。代わりに、マルチGBファイルでは失敗する可能性がありますが、以下をお勧めします。

ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))"

は引用符が続くホワイトスペース(新しい行を含む可能性があります)を探しています。この場合、ホワイトスペースは削除されます。これ%q(')は、引用文字を引用するための洗練された方法です。


1

ここではジムのワンライナーの代わりに、今回はスクリプトで

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

このコードは機能しません。投稿する前にテストしてから、作業コードをコピーして貼り付けることをお勧めします。
Tin Man、

@theTinMan可能であれば、公開する前に常にテストします。私はこれをテストしましたが、コメント付きバージョンと同じように機能します。なぜそうならないと思いますか?
ピーター

正規表現を使用する場合は、私の編集を参照してください。テストも実施されています:>)
peter
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.