Cách tìm kiếm văn bản tệp cho một mẫu và thay thế nó bằng một giá trị nhất định


117

Tôi đang tìm một tập lệnh để tìm kiếm một tệp (hoặc danh sách các tệp) cho một mẫu và nếu tìm thấy, hãy thay thế mẫu đó bằng một giá trị nhất định.

Suy nghĩ?


1
Trong các câu trả lời bên dưới, hãy lưu ý rằng mọi khuyến nghị sử dụng đều File.readcần được xem xét với thông tin trong stackoverflow.com/a/25189286/128421 để biết lý do tại sao việc tải tệp lớn lại có hại. Ngoài ra, thay vì File.open(filename, "w") { |file| file << content }sử dụng các biến thể File.write(filename, content).
the Tin Man

Câu trả lời:


190

Tuyên bố từ chối trách nhiệm: Cách tiếp cận này là một minh họa ngây thơ về khả năng của Ruby, và không phải là giải pháp cấp sản xuất để thay thế các chuỗi trong tệp. Nó dễ xảy ra các tình huống lỗi khác nhau, chẳng hạn như mất dữ liệu trong trường hợp sự cố, gián đoạn hoặc đĩa đầy. Mã này không phù hợp với bất kỳ thứ gì ngoài tập lệnh một lần nhanh chóng nơi tất cả dữ liệu được sao lưu. Vì lý do đó, KHÔNG sao chép mã này vào các chương trình của bạn.

Đây là một cách ngắn gọn để làm điều đó.

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

Có đặt ghi thay đổi trở lại tệp không? Tôi nghĩ rằng điều đó sẽ chỉ in nội dung vào bảng điều khiển.
Dane O'Connor

Có, nó in nội dung ra bảng điều khiển.
sepp2k

7
Vâng, tôi không chắc đó là những gì bạn muốn. Để ghi, hãy sử dụng File.open (file_name, "w") {| file | file.puts output_of_gsub}
Max Chernyak

7
Tôi phải sử dụng file.write: File.open (file_name, "w") {| file | file.write (text)}
2012

3
Để ghi tệp, hãy thay thế put 'line bằngFile.write(file_name, text.gsub(/regexp/, "replace")
chặt chẽ

106

Trên thực tế, Ruby có một tính năng chỉnh sửa tại chỗ. Giống như Perl, bạn có thể nói

ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt

Điều này sẽ áp dụng mã trong dấu ngoặc kép cho tất cả các tệp trong thư mục hiện tại có tên kết thúc bằng ".txt". Bản sao lưu của các tệp đã chỉnh sửa sẽ được tạo bằng phần mở rộng ".bak" (Tôi nghĩ là "foobar.txt.bak").

LƯU Ý: điều này dường như không hoạt động đối với các tìm kiếm nhiều dòng. Đối với những thứ đó, bạn phải làm theo cách khác ít đẹp hơn, với một script wrapper xung quanh regex.


1
Pi.bak là cái quái gì vậy? Nếu không có điều đó, tôi nhận được một lỗi. -e: 1: in<main>': undefined method gsub' cho chính: Object (NoMethodError)
Ninad

15
@NinadPachpute -ichỉnh sửa tại chỗ. .baklà phần mở rộng được sử dụng cho tệp sao lưu (tùy chọn). -plà một cái gì đó giống như while gets; <script>; puts $_; end. ($_ là dòng được đọc cuối cùng, nhưng bạn có thể gán cho nó cho một cái gì đó như thế echo aa | ruby -p -e '$_.upcase!'.)
Lri

1
Đây là câu trả lời tốt hơn câu trả lời được chấp nhận, IMHO, nếu bạn đang tìm cách sửa đổi tệp.
Colin K

6
Làm thế nào tôi có thể sử dụng điều này trong một tập lệnh ruby ​​??
Saurabh

1
Có rất nhiều cách để điều này xảy ra sai, vì vậy hãy kiểm tra kỹ trước khi thử đối với một tệp quan trọng.
the Tin Man

49

Hãy nhớ rằng, khi bạn làm điều này, hệ thống tệp có thể hết dung lượng và bạn có thể tạo tệp có độ dài bằng không. Điều này thật thảm khốc nếu bạn đang làm điều gì đó như viết ra các tệp / etc / passwd như một phần của quản lý cấu hình hệ thống.

Lưu ý rằng chỉnh sửa tệp tại chỗ như trong câu trả lời được chấp nhận sẽ luôn cắt bớt tệp và ghi ra tệp mới theo trình tự. Sẽ luôn có một điều kiện chạy đua trong đó người đọc đồng thời sẽ thấy một tệp bị cắt ngắn. Nếu quá trình bị hủy bỏ vì bất kỳ lý do gì (ctrl-c, OOM kill, sự cố hệ thống, mất điện, v.v.) trong khi ghi thì tệp bị cắt cũng sẽ còn sót lại, điều này có thể rất thảm khốc. Đây là loại kịch bản dữ liệu mà các nhà phát triển PHẢI cân nhắc vì nó sẽ xảy ra. Vì lý do đó, tôi nghĩ câu trả lời được chấp nhận rất có thể không phải là câu trả lời được chấp nhận. Ở mức tối thiểu, hãy ghi vào một tệp tạm thời và di chuyển / đổi tên tệp vào vị trí giống như giải pháp "đơn giản" ở cuối câu trả lời này.

Bạn cần sử dụng một thuật toán:

  1. Đọc tệp cũ và ghi ra tệp mới. (Bạn cần phải cẩn thận về việc chuyển toàn bộ tệp vào bộ nhớ).

  2. Đóng tệp tạm thời mới một cách rõ ràng, đây là nơi bạn có thể ném một ngoại lệ vì bộ đệm tệp không thể được ghi vào đĩa vì không có dung lượng. (Nắm bắt điều này và dọn dẹp tệp tạm thời nếu bạn muốn, nhưng bạn cần phải ném lại thứ gì đó hoặc thất bại khá khó khăn vào thời điểm này.

  3. Sửa các quyền và chế độ tệp trên tệp mới.

  4. Đổi tên tệp mới và đặt nó vào vị trí.

Với hệ thống tệp ext3, bạn được đảm bảo rằng siêu dữ liệu ghi để di chuyển tệp vào đúng vị trí sẽ không bị hệ thống tệp sắp xếp lại và được ghi trước khi bộ đệm dữ liệu cho tệp mới được ghi, vì vậy điều này sẽ thành công hoặc không thành công. Hệ thống tệp ext4 cũng đã được vá để hỗ trợ loại hành vi này. Nếu bạn rất hoang tưởng, bạn nên gọi lệnh gọi fdatasync()hệ thống như là bước 3.5 trước khi chuyển tệp vào vị trí.

Bất kể ngôn ngữ, đây là phương pháp hay nhất. Trong các ngôn ngữ mà việc gọi close()không ném ra ngoại lệ (Perl hoặc C), bạn phải kiểm tra rõ ràng việc trả về close()và ném ngoại lệ nếu nó không thành công.

Đề xuất ở trên chỉ cần chuyển tệp vào bộ nhớ, thao tác và ghi tệp ra tệp sẽ được đảm bảo tạo ra các tệp có độ dài bằng 0 trên hệ thống tệp đầy đủ. Bạn cần luôn sử dụng FileUtils.mvđể di chuyển một tệp tạm thời được viết đầy đủ vào vị trí.

Cân nhắc cuối cùng là vị trí của tệp tạm thời. Nếu bạn mở một tệp trong / tmp thì bạn phải xem xét một số vấn đề:

  • Nếu / tmp được gắn kết trên một hệ thống tệp khác, bạn có thể chạy / tmp hết dung lượng trước khi bạn ghi ra tệp mà nếu không thì tệp có thể triển khai đến đích của tệp cũ.

  • Có lẽ quan trọng hơn, khi bạn cố gắng chuyển mvđổi tệp trên một thiết bị gắn kết, bạn sẽ được chuyển đổi thànhcp hành vi một cách rõ ràng. Tệp cũ sẽ được mở, các tệp cũ inode sẽ được giữ nguyên và mở lại và nội dung tệp sẽ được sao chép. Đây rất có thể không phải là điều bạn muốn và bạn có thể gặp phải lỗi "tệp văn bản bận" nếu bạn cố gắng chỉnh sửa nội dung của tệp đang chạy. Điều này cũng làm mất đi mục đích của việc sử dụng các mvlệnh hệ thống tệp và bạn có thể chạy hệ thống tệp đích hết dung lượng chỉ với một tệp được ghi một phần.

    Điều này cũng không liên quan gì đến việc triển khai của Ruby. Hệ thống mvcpcác lệnh hoạt động tương tự.

Điều thích hợp hơn là mở Tempfile trong cùng thư mục với tệp cũ. Điều này đảm bảo rằng sẽ không có vấn đề di chuyển giữa các thiết bị. Cácmv thân nó không bao giờ bị lỗi, và bạn sẽ luôn nhận được một tệp hoàn chỉnh và không bị xử lý. Bất kỳ lỗi nào, chẳng hạn như thiết bị hết dung lượng, lỗi quyền, v.v., sẽ gặp phải trong quá trình ghi Tempfile ra ngoài.

Nhược điểm duy nhất của phương pháp tạo Tempfile trong thư mục đích là:

  • Đôi khi bạn có thể không mở được Tempfile ở đó, chẳng hạn như nếu bạn đang cố gắng 'chỉnh sửa' một tệp trong / proc chẳng hạn. Vì lý do đó, bạn có thể muốn quay lại và thử / tmp nếu không mở được tệp trong thư mục đích.
  • Bạn phải có đủ dung lượng trên phân vùng đích để chứa cả tệp cũ hoàn chỉnh và tệp mới. Tuy nhiên, nếu bạn không có đủ dung lượng để chứa cả hai bản sao thì có thể bạn đang thiếu dung lượng đĩa và nguy cơ thực tế khi ghi tệp bị cắt ngắn cao hơn nhiều, vì vậy tôi cho rằng đây là một sự cân bằng rất kém ngoài một số cực kỳ hẹp (và tốt -monitored) các trường hợp cạnh.

Đây là một số mã triển khai thuật toán đầy đủ (mã windows chưa được kiểm tra và chưa hoàn thành):

#!/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")

Và đây là một phiên bản chặt chẽ hơn một chút không lo lắng về mọi trường hợp cạnh có thể xảy ra (nếu bạn đang sử dụng Unix và không quan tâm đến việc ghi vào / 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")

Trường hợp sử dụng thực sự đơn giản, khi bạn không quan tâm đến quyền của hệ thống tệp (bạn không chạy với quyền root hoặc bạn đang chạy với quyền root và tệp thuộc quyền sở hữu của 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 : Tối thiểu phải được sử dụng thay cho câu trả lời được chấp nhận, trong mọi trường hợp, để đảm bảo bản cập nhật là nguyên tử và người đọc đồng thời sẽ không thấy các tệp bị cắt bớt. Như tôi đã đề cập ở trên, việc tạo Tempfile trong cùng thư mục với tệp đã chỉnh sửa là điều quan trọng ở đây để tránh các hoạt động mv thiết bị chéo được dịch thành các hoạt động cp nếu / tmp được gắn trên một thiết bị khác. Gọi fdatasync là một lớp hoang tưởng bổ sung, nhưng nó sẽ gây ảnh hưởng đến hiệu suất, vì vậy tôi đã bỏ qua nó khỏi ví dụ này vì nó không thường được thực hành.


Thay vì mở một tập tin tạm thời trong thư mục mà bạn đang ở trong đó sẽ thực sự tự động tạo ra một trong thư mục dữ liệu ứng dụng (trên Windows anyways) và từ họ, bạn có thể làm một file.unlink để xóa nó ..
13aal

3
Tôi thực sự đánh giá cao những suy nghĩ bổ sung đã được đưa vào này. Là một người mới bắt đầu, sẽ rất thú vị khi xem các mô hình suy nghĩ của các nhà phát triển có kinh nghiệm, những người không chỉ trả lời câu hỏi ban đầu mà còn nhận xét về bối cảnh lớn hơn về ý nghĩa thực sự của câu hỏi ban đầu.
ramijames

Lập trình không chỉ là giải quyết vấn đề trước mắt mà còn là cách suy nghĩ trước để tránh những vấn đề khác đang chờ đợi. Không có gì khó chịu với một nhà phát triển cấp cao hơn là gặp phải mã vẽ thuật toán vào một góc, buộc một sự khó xử, khi một điều chỉnh nhỏ trước đó có thể dẫn đến một dòng chảy tốt đẹp. Thường có thể mất hàng giờ hoặc vài ngày phân tích để hiểu được mục tiêu và sau đó một vài dòng thay thế một trang mã cũ. Nó giống như một ván cờ đối đầu với dữ liệu và hệ thống.
the Tin Man

11

Thực sự không có cách nào để chỉnh sửa tệp tại chỗ. Những gì bạn thường làm khi có thể thoát khỏi nó (tức là nếu tệp không quá lớn) là bạn đọc tệp vào bộ nhớ ( File.read), thực hiện các thay thế của bạn trên chuỗi đọc ( String#gsub) và sau đó ghi chuỗi đã thay đổi trở lại tệp ( File.open, File#write).

Nếu các tập tin đủ lớn cho rằng không khả thi, những gì bạn cần phải làm là đọc các tập tin trong khối (nếu mô hình bạn muốn thay thế sẽ không span nhiều dòng sau đó một đoạn thường có nghĩa là một dòng - bạn có thể sử dụng File.foreachđể đọc từng dòng một của tệp), và đối với mỗi đoạn, hãy thực hiện thay thế tệp đó và nối tệp đó vào tệp tạm thời. Khi bạn hoàn tất việc lặp lại tệp nguồn, bạn đóng nó và sử dụng FileUtils.mvđể ghi đè nó bằng tệp tạm thời.


1
Tôi thích cách tiếp cận phát trực tuyến. Chúng ta đối phó với các tập tin lớn đồng thời nên chúng tôi không thường có không gian trong RAM để đọc toàn bộ tập tin
Shane

" Tại sao" slurping "một tệp không phải là một phương pháp hay? " Có thể là cách đọc hữu ích liên quan đến vấn đề này.
the Tin Man

9

Một cách tiếp cận khác là sử dụng chỉnh sửa tại chỗ bên trong Ruby (không phải từ dòng lệnh):

#!/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

Nếu bạn không muốn tạo bản sao lưu thì hãy thay đổi '.bak'thành ''.


1
Điều này sẽ tốt hơn là cố gắng slurp ( read) tệp. Nó có thể mở rộng và sẽ rất nhanh.
the Tin Man

Có một lỗi ở đâu đó khiến Ruby 2.3.0p0 trên Windows không thành công với quyền bị từ chối nếu có nhiều khối inplace_edit liên tiếp hoạt động trên cùng một tệp. Để tái tạo các bài kiểm tra search1 và search2 chia thành 2 khối. Không đóng cửa hoàn toàn?
mlt

Tôi mong đợi sự cố với nhiều lần chỉnh sửa tệp văn bản xảy ra đồng thời. Nếu không có gì khác, bạn có thể nhận được một tệp văn bản bị sai lệch nghiêm trọng.
the Tin Man

7

Điều này phù hợp với tôi:

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

6

Đây là một giải pháp để tìm / thay thế trong tất cả các tệp của một thư mục nhất định. Về cơ bản, tôi đã lấy câu trả lời do sepp2k cung cấp và mở rộng nó.

# 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
Sẽ hữu ích hơn nếu bạn cung cấp lời giải thích tại sao đây là giải pháp ưa thích và giải thích cách hoạt động của nó. Chúng tôi muốn giáo dục, không chỉ cung cấp mã.
the Tin Man

trollop đã được đổi tên thành lạc quan github.com/manageiq/optimist . Ngoài ra, nó chỉ là một trình phân tích cú pháp tùy chọn CLI không thực sự cần thiết để trả lời câu hỏi.
noraj

1

Nếu bạn cần thay thế trên các đường ranh giới, thì việc sử dụng ruby -pi -esẽ không hoạt động vì các pquy trình xử lý từng dòng một. Thay vào đó, tôi khuyên bạn nên làm như sau, mặc dù nó có thể không thành công với tệp nhiều GB:

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

Tìm kiếm khoảng trắng (có thể bao gồm các dòng mới) theo sau một câu trích dẫn, trong trường hợp đó, nó sẽ loại bỏ khoảng trắng. Đây %q(')chỉ là một cách trích dẫn lạ mắt của nhân vật trích dẫn.


1

Đây là một giải pháp thay thế cho một lớp lót từ jim, lần này là trong một tập lệnh

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

Lưu nó trong một script, ví dụ như Replace.rb

Bạn bắt đầu trên dòng lệnh với

replace.rb *.txt <string_to_replace> <replacement>

* .txt có thể được thay thế bằng một lựa chọn khác hoặc bằng một số tên tệp hoặc đường dẫn

được chia nhỏ để tôi có thể giải thích những gì đang xảy ra nhưng vẫn có thể thực thi

# 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

CHỈNH SỬA: nếu bạn muốn sử dụng biểu thức chính quy, hãy sử dụng biểu thức này để thay thế Rõ ràng, điều này chỉ để xử lý các tệp văn bản tương đối nhỏ, không có quái vật Gigabyte

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

Mã này sẽ không hoạt động. Tôi khuyên bạn nên kiểm tra nó trước khi đăng, sau đó sao chép và dán mã làm việc.
Tin Man

@theTinMan Tôi luôn kiểm tra trước khi xuất bản, nếu có thể. Tôi đã thử nghiệm điều này và nó hoạt động, cả hai đều ngắn như phiên bản đã nhận xét. Tại sao bạn nghĩ rằng nó sẽ không?
peter

nếu bạn có nghĩa là sử dụng một biểu thức chính quy, hãy xem bản chỉnh sửa của tôi, cũng đã được thử nghiệm:>)
peter
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.