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.
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
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.
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:
Lê o arquivo antigo e grava no novo arquivo. (Você precisa ter cuidado ao colocar arquivos inteiros na memória).
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.
Corrige as permissões e modos de arquivo no novo arquivo.
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):
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):
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):
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.
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
Outra abordagem é usar a edição local dentro do Ruby (não na linha de comando):
#!/usr/bin/rubydef 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 ''.
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.
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
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:
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.
* .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 2File.write(f,# open the argument (= filename) for writingFile.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
File.read
precisam 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 deFile.open(filename, "w") { |file| file << content }
variações, useFile.write(filename, content)
.Respostas:
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.
fonte
File.write(file_name, text.gsub(/regexp/, "replace")
Na verdade, Ruby tem um recurso de edição local. Como Perl, você pode dizer
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.
fonte
<main>': undefined method
gsub 'para main: Object (NoMethodError)-i
edita em vigor..bak
é a extensão usada para um arquivo de backup (opcional).-p
é algo parecidowhile gets; <script>; puts $_; end
. ($_
é a última linha lida, mas você pode atribuir a ela para algo comoecho aa | ruby -p -e '$_.upcase!'
.)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:
Lê o arquivo antigo e grava no novo arquivo. (Você precisa ter cuidado ao colocar arquivos inteiros na memória).
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.
Corrige as permissões e modos de arquivo no novo arquivo.
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 declose()
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.mv
para 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
mv
montar o arquivo em um dispositivo, você será convertido emcp
comportamento 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 osmv
comandos 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
mv
e oscp
comandos 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
mv
pró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:
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):
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):
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):
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.
fonte
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.foreach
para 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 useFileUtils.mv
para substituí-lo pelo arquivo temporário.fonte
Outra abordagem é usar a edição local dentro do Ruby (não na linha de comando):
Se você não quiser criar um backup, mude
'.bak'
para''
.fonte
read
) o arquivo. É escalonável e deve ser muito rápido.Isso funciona para mim:
fonte
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.
fonte
fonte
Se você precisar fazer substituições além dos limites das linhas, o uso
ruby -pi -e
não funcionará porque osp
processos são feitos uma linha por vez. Em vez disso, recomendo o seguinte, embora possa falhar com um arquivo de vários GB: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.fonte
Aqui, uma alternativa ao forro de Jim, desta vez em um script
Salve-o em um script, por exemplo, replace.rb
Você começa na linha de comando com
* .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
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
fonte