Como pesquisar o texto do arquivo por um padrão e substituí-lo por um determinado valor


117

Estou procurando um script para pesquisar um arquivo (ou lista de arquivos) por um padrão e, se encontrado, substituir esse padrão por um determinado valor.

Pensamentos?


1
Nas respostas abaixo, esteja ciente de que todas as recomendações de uso File.readprecisam ser moderadas com as informações em stackoverflow.com/a/25189286/128421 sobre o motivo pelo qual slurping arquivos grandes é ruim. Além disso, em vez de File.open(filename, "w") { |file| file << content }variações, use File.write(filename, content).
o Homem de Lata de

Respostas:


190

Isenção de responsabilidade: esta abordagem é uma ilustração ingênua dos recursos do Ruby, e não uma solução de nível de produção para substituir strings em arquivos. Está sujeito a vários cenários de falha, como perda de dados em caso de falha, interrupção ou disco cheio. Este código não serve para nada além de um script rápido e único em que é feito o backup de todos os dados. Por esse motivo, NÃO copie este código em seus programas.

Aqui está uma maneira rápida e curta de fazer isso.

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

O puts grava a alteração de volta no arquivo? Achei que seria apenas imprimir o conteúdo no console.
Dane O'Connor

Sim, ele imprime o conteúdo no console.
sepp2k

7
Sim, eu não tinha certeza se era isso que você queria. Para escrever, use File.open (file_name, "w") {| file | file.puts output_of_gsub}
Max Chernyak

7
Tive que usar file.write: File.open (file_name, "w") {| file | file.write (text)}
austen

3
Para gravar o arquivo, substitua puts 'linha porFile.write(file_name, text.gsub(/regexp/, "replace")
tight

106

Na verdade, Ruby tem um recurso de edição local. Como Perl, você pode dizer

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

Isso aplicará o código entre aspas a todos os arquivos no diretório atual cujos nomes terminem com ".txt". As cópias de segurança dos arquivos editados serão criadas com uma extensão ".bak" ("foobar.txt.bak" eu acho).

NOTA: isso não parece funcionar para pesquisas em várias linhas. Para eles, você tem que fazer de outra maneira menos bonita, com um script wrapper em torno da regex.


1
O que diabos é pi.bak? Sem isso, recebo um erro. -e: 1: in <main>': undefined method gsub 'para main: Object (NoMethodError)
Ninad

15
@NinadPachpute -iedita em vigor. .baké a extensão usada para um arquivo de backup (opcional). -pé algo parecido while gets; <script>; puts $_; end. ( $_é a última linha lida, mas você pode atribuir a ela para algo como echo aa | ruby -p -e '$_.upcase!'.)
Lri

1
Esta é uma resposta melhor do que a resposta aceita, IMHO, se você deseja modificar o arquivo.
Colin K de

6
Como posso usar isso dentro de um script ruby ​​??
Saurabh

1
Isso pode dar errado de várias maneiras, então teste-o completamente antes de tentar contra um arquivo crítico.
The Tin Man

49

Tenha em mente que, ao fazer isso, o sistema de arquivos pode ficar sem espaço e você pode criar um arquivo de comprimento zero. Isso é catastrófico se você estiver fazendo algo como gravar arquivos / etc / passwd como parte do gerenciamento de configuração do sistema.

Observe que a edição local do arquivo, como na resposta aceita, sempre truncará o arquivo e gravará o novo arquivo sequencialmente. Sempre haverá uma condição de corrida em que os leitores simultâneos verão um arquivo truncado. Se o processo for abortado por qualquer motivo (ctrl-c, OOM killer, falha do sistema, queda de energia, etc) durante a gravação, o arquivo truncado também será deixado, o que pode ser catastrófico. Este é o tipo de cenário de perda de dados que os desenvolvedores DEVEM considerar porque isso vai acontecer. Por esse motivo, acho que a resposta aceita provavelmente não deve ser a resposta aceita. No mínimo, grave em um arquivo temporário e mova / renomeie o arquivo no lugar como a solução "simples" no final desta resposta.

Você precisa usar um algoritmo que:

  1. Lê o arquivo antigo e grava no novo arquivo. (Você precisa ter cuidado ao colocar arquivos inteiros na memória).

  2. Fecha explicitamente o novo arquivo temporário, que é onde você pode lançar uma exceção porque os buffers de arquivo não podem ser gravados no disco porque não há espaço. (Pegue isso e limpe o arquivo temporário se desejar, mas você precisa relançar algo ou falhar bastante neste ponto.

  3. Corrige as permissões e modos de arquivo no novo arquivo.

  4. Renomeia o novo arquivo e o coloca no lugar.

Com sistemas de arquivos ext3, você tem a garantia de que a gravação de metadados para mover o arquivo para o local não será reorganizada pelo sistema de arquivos e gravada antes que os buffers de dados para o novo arquivo sejam gravados, então isso deve ser bem-sucedido ou falhar. O sistema de arquivos ext4 também foi corrigido para suportar esse tipo de comportamento. Se você é muito paranóico, deve chamar a chamada de fdatasync()sistema como uma etapa 3.5 antes de mover o arquivo para o lugar.

Independentemente do idioma, essa é a prática recomendada. Em linguagens em que a chamada close()não lança uma exceção (Perl ou C), você deve verificar explicitamente o retorno de close()e lançar uma exceção se ele falhar.

A sugestão acima de simplesmente engolir o arquivo na memória, manipulá-lo e gravá-lo no arquivo terá a garantia de produzir arquivos de comprimento zero em um sistema de arquivos completo. Você sempre precisa usar FileUtils.mvpara mover um arquivo temporário totalmente escrito para o lugar.

Uma consideração final é a colocação do arquivo temporário. Se você abrir um arquivo em / tmp, deverá considerar alguns problemas:

  • Se / tmp estiver montado em um sistema de arquivos diferente, você pode executar / tmp sem espaço antes de gravar o arquivo que, de outra forma, seria implantado no destino do arquivo antigo.

  • Provavelmente, o mais importante é que ao tentar mvmontar o arquivo em um dispositivo, você será convertido em cpcomportamento de forma transparente . O arquivo antigo será aberto, o inode dos arquivos antigos será preservado e reaberto e o conteúdo do arquivo será copiado. Provavelmente não é isso que você deseja, e você pode encontrar erros de "arquivo de texto ocupado" se tentar editar o conteúdo de um arquivo em execução. Isso também anula o propósito de usar os mvcomandos do sistema de arquivos e você pode executar o sistema de arquivos de destino sem espaço com apenas um arquivo parcialmente escrito.

    Isso também não tem nada a ver com a implementação de Ruby. O sistema mve os cpcomandos se comportam de maneira semelhante.

O que é mais preferível é abrir um arquivo Temp no mesmo diretório do arquivo antigo. Isso garante que não haverá problemas de movimentação entre dispositivos. O mvpróprio arquivo nunca deve falhar e você sempre deve obter um arquivo completo e não truncado. Quaisquer falhas, como dispositivo sem espaço, erros de permissão, etc., devem ser encontradas durante a gravação do arquivo Temp.

As únicas desvantagens para a abordagem de criação do Tempfile no diretório de destino são:

  • Às vezes, você pode não conseguir abrir um arquivo Temp, como se estivesse tentando 'editar' um arquivo em / proc, por exemplo. Por esse motivo, você pode querer voltar e tentar / tmp se abrir o arquivo no diretório de destino falhar.
  • Você deve ter espaço suficiente na partição de destino para armazenar o arquivo antigo completo e o novo. No entanto, se você tiver espaço insuficiente para armazenar ambas as cópias, então provavelmente você está com pouco espaço em disco e o risco real de gravar um arquivo truncado é muito maior, então eu diria que esta é uma troca muito pobre fora de alguns excessivamente estreitos (e bem -monitorados) casos extremos.

Aqui está um código que implementa o algoritmo completo (o código do Windows não foi testado e não foi concluído):

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

E aqui está uma versão um pouco mais restrita que não se preocupa com todos os casos extremos possíveis (se você estiver no Unix e não se importar em escrever em / 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")

O caso de uso realmente simples, para quando você não se importa com as permissões do sistema de arquivos (ou você não está executando como root ou está executando como root e o arquivo é de propriedade do 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 : Deve ser usado no mínimo em vez da resposta aceita, em todos os casos, a fim de garantir que a atualização seja atômica e que os leitores simultâneos não vejam os arquivos truncados. Como mencionei acima, criar o Tempfile no mesmo diretório do arquivo editado é importante aqui para evitar que as operações mv entre dispositivos sejam traduzidas em operações cp se / tmp for montado em um dispositivo diferente. Chamar fdatasync é uma camada adicional de paranóia, mas causará um impacto no desempenho, portanto, omiti neste exemplo, uma vez que não é comumente praticado.


Em vez de abrir um arquivo temporário no diretório em que você está, ele criará automaticamente um no diretório de dados do aplicativo (no Windows de qualquer maneira) e, a partir dele, você pode fazer um arquivo.unlink para excluí-lo.
13aal

3
Eu realmente apreciei o pensamento extra que foi colocado nisso. Como um iniciante, é muito interessante ver os padrões de pensamento de desenvolvedores experientes que podem não apenas responder à pergunta original, mas também comentar sobre o contexto mais amplo do que a pergunta original realmente significa.
ramijames

Programar não trata apenas de consertar o problema imediato, mas também de pensar com antecedência para evitar outros problemas à espreita. Nada irrita mais um desenvolvedor sênior do que encontrar um código que encurralou o algoritmo, forçando um erro estranho, quando um pequeno ajuste anterior teria resultado em um bom fluxo. Muitas vezes, pode levar horas ou dias de análise para entender a meta e, em seguida, algumas linhas substituem uma página de código antigo. Às vezes é como um jogo de xadrez contra os dados e o sistema.
The Tin Man

11

Não há realmente uma maneira de editar arquivos no local. O que você geralmente faz quando consegue se safar (ou seja, se os arquivos não são muito grandes) é ler o arquivo na memória ( File.read), realizar suas substituições na string de leitura ( String#gsub) e, em seguida, gravar a string alterada de volta no arquivo ( File.open, File#write).

Se os arquivos forem grandes o suficiente para que isso seja inviável, o que você precisa fazer é ler o arquivo em pedaços (se o padrão que você deseja substituir não abranger várias linhas, então um pedaço geralmente significa uma linha - você pode usar File.foreachpara ler um arquivo linha por linha) e, para cada pedaço, faça a substituição nele e anexe-o a um arquivo temporário. Quando terminar de iterar o arquivo de origem, feche-o e use FileUtils.mvpara substituí-lo pelo arquivo temporário.


1
Eu gosto da abordagem de streaming. Lidamos com arquivos grandes ao mesmo tempo, portanto, geralmente não temos espaço na RAM para ler o arquivo inteiro
Shane

" Por que" engolir "um arquivo não é uma boa prática? " Pode ser uma leitura útil em relação a isso.
o homem de lata

9

Outra abordagem é usar a edição local dentro do Ruby (não na linha de comando):

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

Se você não quiser criar um backup, mude '.bak'para ''.


1
Isso seria melhor do que tentar slurp ( read) o arquivo. É escalonável e deve ser muito rápido.
o Tin Man de

Há um bug em algum lugar que faz com que o Ruby 2.3.0p0 no Windows falhe com permissão negada se houver vários blocos inplace_edit consecutivos trabalhando no mesmo arquivo. Para reproduzir os testes de pesquisa1 e pesquisa2 divididos em 2 blocos. Não fechando completamente?
mlt

Eu esperaria problemas com várias edições de um arquivo de texto ocorrendo simultaneamente. Se nada mais, você poderia obter um arquivo de texto malformado.
O Homem de Lata de

7

Isso funciona para mim:

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

6

Aqui está uma solução para localizar / substituir em todos os arquivos de um determinado diretório. Basicamente, peguei a resposta fornecida por sepp2k e a expandi.

# 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
Será mais útil se você explicar por que essa é a solução preferida e explicar como ela funciona. Queremos educar, não apenas fornecer código.
The Tin Man

trollop foi renomeado como otimista para github.com/manageiq/optimist . Além disso, é apenas um analisador de opções CLI não realmente necessário para responder à pergunta.
noraj

1

Se você precisar fazer substituições além dos limites das linhas, o uso ruby -pi -enão funcionará porque os pprocessos são feitos uma linha por vez. Em vez disso, recomendo o seguinte, embora possa falhar com um arquivo de vários GB:

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

O está procurando por um espaço em branco (potencialmente incluindo novas linhas) seguido por uma citação, caso em que elimina o espaço em branco. Essa %q(')é apenas uma maneira elegante de citar o personagem de citação.


1

Aqui, uma alternativa ao forro de Jim, desta vez em um script

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

Salve-o em um script, por exemplo, replace.rb

Você começa na linha de comando com

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

* .txt pode ser substituído por outra seleção ou por alguns nomes de arquivos ou caminhos

dividido para que eu possa explicar o que está acontecendo, mas ainda executável

# 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

EDITAR: se você quiser usar uma expressão regular, use isso. Obviamente, isso é apenas para lidar com arquivos de texto relativamente pequenos, sem monstros Gigabyte

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

Este código não funcionará. Sugiro testá-lo antes de postar e, em seguida, copie e cole o código de trabalho.
The Tin Man

@theTinMan Eu sempre testo antes de publicar, se possível. Testei e deu certo, tanto na versão curta quanto na comentada. Por que você acha que não?
Pedro

se você quer dizer usar uma expressão regular, veja minha edição, também testei:>)
peter
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.