ferramenta não orientada à linha para substituição de strings?

13

Recentemente, perguntei a uma pergunta sobre como remover o caractere de nova linha se ele ocorrer após outro caractere específico.

As ferramentas de processamento de texto Unix são muito poderosas, mas quase todas elas lidam com linhas de texto, o que é bom na maioria das vezes quando a entrada se encaixa na memória disponível.

Mas o que devo fazer se quiser substituir uma sequência de texto em um arquivo enorme que não contenha novas linhas?

Por exemplo, substitua <foobar> por \n<foobar> sem ler a entrada linha por linha? (já que existe apenas uma linha e tem 2.5G caracteres).

    
por MattBianco 16.06.2014 / 16:32

5 respostas

9

gsar (pesquisa geral e substituição) é uma ferramenta muito útil exatamente para esse propósito .

A maioria das respostas a essa pergunta usa ferramentas baseadas em registros e vários truques para adaptá-las ao problema, como alternar o caractere separador de registro padrão para algo assumido como ocorrendo com freqüência suficiente na entrada para não tornar cada registro muito grande para lidar.

Em muitos casos, isso é muito bom e até mesmo legível. Eu gosto de problemas que podem ser facilmente / eficientemente resolvidos com ferramentas disponíveis em todos os lugares, como awk , tr , sed e o shell bourne.

Realizar uma pesquisa binária e substituir em um arquivo grande e arbitrário com conteúdo aleatório não se encaixa muito bem com essas ferramentas unix padrão.

Alguns de vocês podem pensar que isso é uma fraude, mas não vejo como o uso da ferramenta certa para o trabalho pode estar errado. Neste caso, é um programa em C chamado gsar que está licenciado sob GPL v2 , por isso me surpreende que não exista nenhum pacote para esta ferramenta muito útil em nenhum gentoo , redhat , nem ubuntu .

gsar usa uma variante binária do algoritmo de pesquisa de strings Boyer-Moore .

O uso é direto:

gsar -F '-s<foobar>' '-r:x0A<foobar>'

em que -F significa o modo "filtro", ou seja, leia stdin em stdout . Existem métodos para operar em arquivos também. -s especifica a string de pesquisa e -r da substituição. A notação de dois pontos pode ser usada para especificar valores arbitrários de byte.

O modo que não diferencia maiúsculas de minúsculas é suportado ( -i ), mas não há suporte para expressões regulares, já que o algoritmo usa o tamanho da string de pesquisa para otimizar a pesquisa.

A ferramenta também pode ser usada apenas para pesquisa, um pouco como grep . gsar -b exibe os deslocamentos de byte da string de pesquisa correspondente e gsar -l imprime nome do arquivo e número de correspondências, se houver, um pouco como combinar grep -l com wc .

A ferramenta foi escrita por Tormod Tjaberg (inicial) e Hans Peter Verne (melhorias).

    
por 17.06.2014 / 13:59
12

A primeira coisa que me ocorre quando enfrento esse tipo de problema é alterar o separador de registro. Na maioria das ferramentas, isso é definido como \n por padrão, mas isso pode ser alterado. Por exemplo:

  1. Perl

    perl -0x3E -pe 's/<foobar>/\n$&/' file
    

    Explicação

    • -0 : define o separador de registro de entrada como um caractere, dado seu valor hexadecimal . Nesse caso, estou configurando para > cujo valor hexadecimal é 3E . O formato geral é -0xHEX_VALUE . Este é apenas um truque para quebrar a linha em pedaços gerenciáveis.
    • -pe : imprima cada linha de entrada depois de aplicar o script fornecido por -e .
    • s/<foobar>/\n$&/ : uma simples substituição. O $& é o que foi correspondido, neste caso <foobar> .
  2. awk

    awk '{gsub(/foobar>/,"\n<foobar>");printf "%s",$0};' RS="<" file
    

    Explicação

    • RS="<" : define o separador de registro de entrada como > .
    • gsub(/foobar>/,"\n<foobar>") : substitua todos os casos de foobar> por \n<foobar> . Observe que, como RS foi definido como < , todos os < foram removidos do arquivo de entrada (é assim que awk funciona), então precisamos corresponder foobar> (sem < ) e substituir por \n<foobar> .
    • printf "%s",$0 : imprime a "linha" atual após a substituição. $0 é o registro atual em awk , por isso, ele armazenará o que quer que fosse antes de < .

Eu testei isso em um arquivo de linha única de 2,3 GB criado com estes comandos:

for i in {1..900000}; do printf "blah blah <foobar>blah blah"; done > file
for i in {1..100}; do cat file >> file1; done
mv file1 file

Tanto o awk quanto o perl usaram quantidades insignificantes de memória.

    
por 16.06.2014 / 17:18
5

No caso restrito em que as strings de destino e de substituição são do mesmo tamanho, o mapeamento de memória pode ser usado para o resgate. Isso é especialmente útil se a substituição precisar ser executada no local. Você está basicamente mapeando um arquivo para a memória virtual de um processo, e o espaço de endereçamento para endereçamento de 64 bits é enorme. Observe que o arquivo não é necessariamente mapeado na memória física de uma só vez , portanto, arquivos com tempo do tamanho da memória física disponível na máquina podem ser tratados.

Aqui está um exemplo do Python que substitui foobar por XXXXXX

#! /usr/bin/python
import mmap
import contextlib   
with open('test.file', 'r+') as f:
 with contextlib.closing(mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_WRITE)) as m:
   pos = 0
   pos = m.find('foobar', pos)
   while pos > 0:
    m[pos: pos+len('XXXXXX')] = 'XXXXXX'
    pos = m.find('foobar', pos)
    
por 16.06.2014 / 23:28
4

Existem muitas ferramentas para isso:

dd é o que você deseja usar se quiser bloquear um arquivo - ler de maneira confiável apenas um determinado número de bytes apenas um determinado número de vezes. Manipula de maneira portável o bloqueio e o desbloqueio de fluxos de arquivos:

tr -dc '[:graph:]' </dev/urandom | dd bs=32 count=1 cbs=8 conv=unblock,sync 2>/dev/null

###OUTPUT###

UI(#Q5\e BKX2?A:Z RAxGm:qv t!;/v!)N

Eu também uso tr acima porque ele pode manipular a conversão de qualquer byte ASCII para qualquer outro (ou, neste caso, excluir qualquer byte ASCII que não seja um caracter não imprimível no espaço). É o que eu usei em resposta para sua outra pergunta esta manhã, na verdade, quando eu fiz:

tr '>\n' '\n>' | sed 's/^>*//' | tr '\n>' '>\n' 

Existem muitos semelhantes . Essa lista deve fornecer um subconjunto de menor denominador comum com o qual você pode se familiarizar.

Mas, se eu fosse fazer processamento de texto em 2.5gbs de arquivo binário, poderia começar com od . Pode dar-lhe um octal dump ou qualquer um dos vários outros formatos. Você pode especificar todos os tipos de opções - mas só farei um byte por linha em um formato \C escape:

Os dados que você receberá de od serão regulares em qualquer intervalo que você especificar - como mostrarei abaixo. Mas primeiro - aqui está uma resposta para sua pergunta:

printf 'first\nnewline\ttab spacefoobar
first
\nnewline
\ttab
 spacefoobar
printf ... | od ... | sed ... | 
sed 's/foobar/\
&\
/g'

###OUTPUT###

first
\nnewline
\ttab
 space
foobar

printf 'nl\ntab\tspace foobarfoobar
printf 'nl\ntab\tspace foobarfoobar
###OUTPUT###
null' | od -A n -t c -v -w1 n l \n t a b \t s p a c e f o o b a r f o o b a r
tr '>\n' '\n>' | sed 's/^>*//' | tr '\n>' '>\n' 
n u l l
null' | PIPELINE | sed 's/./\&/g' | xargs printf %b | cat -A ###OUTPUT### nl$ tab^Ispace $ foobar$ $ foobar$ ^@null%
null
null
null' | od -A n -t c -v -w1 | sed 's/^ \{1,3\}//;s/\$/&&/;/ /bd /\[0nt]/!{H;$!d};{:d x;s/\n//g}'

Esse pouco acima delimita em \n ewlines, \t nulls, <spaces> abs e \C preservando a string H escaped para o delimitador. Observe as funções x e sed usadas - sempre que sed encontra um delimitador, ele troca o conteúdo de seus buffers de memória. Desta forma, sed apenas retém tanta informação quanto é necessário para delimitar de forma confiável o arquivo e não sucumbir a saturações de buffer - isto é, desde que ele realmente encontre seus delimitadores. Por enquanto, od continuará processando sua entrada e EOF continuará a fornecê-la até encontrar foobar .

Como é, a saída é assim:

printf 'first\nnewline\ttab spacefoobar
first
\nnewline
\ttab
 spacefoobar
printf ... | od ... | sed ... | 
sed 's/foobar/\
&\
/g'

###OUTPUT###

first
\nnewline
\ttab
 space
foobar

printf 'nl\ntab\tspace foobarfoobar
printf 'nl\ntab\tspace foobarfoobar%pre%null' |
od -A n -t c -v -w1

   n
   l
  \n
   t
   a
   b
  \t
   s
   p
   a
   c
   e

   f
   o
   o
   b
   a
   r
   f
   o
   o
   b
   a
   r
  %pre%
   n
   u
   l
   l
null' | PIPELINE | sed 's/./\&/g' | xargs printf %b | cat -A ###OUTPUT### nl$ tab^Ispace $ foobar$ $ foobar$ ^@null%
null
null
null' | od -A n -t c -v -w1 | sed 's/^ \{1,3\}//;s/\$/&&/;/ /bd /\[0nt]/!{H;$!d};{:d x;s/\n//g}'

Então, se eu quiser C :

%pre%

Agora, se você quiser usar o sed escapes, é muito fácil - porque \ tem double printf backslash escapou de todas as suas barras invertidas de entrada única, então xargs execed de xargs terá nenhum problema produzindo a saída para sua especificação. Mas sed come as citações do shell , então você precisará duplicá-lo novamente:

%pre%

Isso poderia ter sido salvo facilmente em uma variável do shell e ser exibido posteriormente de maneira idêntica. O último \ insere uma barra invertida sed antes de cada caractere em sua entrada, e isso é tudo.

E aqui está o que parece antes. %code% se apega:

%pre%     
por 16.06.2014 / 21:36
2

O awk opera em registros sucessivos. Ele pode usar qualquer caractere como o separador de registro (exceto o byte nulo em muitas implementações). Algumas implementações suportam expressões regulares arbitrárias (que não correspondem à string vazia) como separador de registro, mas isso pode ser difícil porque o separador de registro é truncado do final de cada registro antes de ser armazenado em $0 (o GNU awk define a variável RT para o separador de registro que foi retirado do final do registro atual). Observe que print termina sua saída com o separador de registro de saída ORS , que é uma nova linha por padrão e é definido independentemente do separador de registro de entrada RS .

awk -v RS=, 'NR==1 {printf "input up to the first comma: %s\n", $0}'

Você pode efetivamente selecionar um caractere diferente como o separador de registro para outras ferramentas ( sort , sed ,…) trocando novas linhas com esse caractere com tr .

tr '\n,' ',\n' |
sed 's/foo/bar/' |
sort |
tr '\n,' ',\n'

Muitos utilitários de texto GNU suportam o uso de um byte nulo ao invés de uma nova linha como o separador.

    
por 17.06.2014 / 02:57