Substituir sequência contendo nova linha em um arquivo enorme

16

Alguém conhece uma ferramenta não baseada em linha para pesquisar / substituir "binariamente" cadeias de caracteres de maneira um pouco eficiente em termos de memória? Veja esta pergunta também.

Eu tenho um arquivo de texto de + 2 GB que gostaria de processar semelhante ao que isso parece fazer:

sed -e 's/>\n/>/g'

Isso significa que eu quero remover todas as novas linhas que ocorrem depois de um >, mas não em nenhum outro lugar, para que isso exclua tr -d.

Este comando (que obtive da resposta de uma pergunta semelhante ) falha com couldn't re-allocate memory:

sed --unbuffered ':a;N;$!ba;s/>\n/>/g'

Então, existem outros métodos sem recorrer a C? Eu odeio perl, mas estou disposto a fazer uma exceção neste caso :-)

Não tenho certeza de nenhum caractere que não ocorra nos dados; portanto, substituir temporariamente \npor outro é algo que eu gostaria de evitar, se possível.

Alguma boa ideia, alguém?

MattBianco
fonte
Você já tentou a opção --unbuffered?
CTRL-ALT-DELOR
Com ou sem --unbufferedficar sem memória
Matt Bianco
O que $!faz?
CTRL-ALT-DELOR
O que há de errado com o primeiro comando sed. O segundo parece estar lendo tudo no espaço padrão, mas não sei o que $!é. Espero que isso precise de muita memória.
CTRL-ALT-DELOR
O problema é que o sed lê tudo como linhas, é por isso que o primeiro comando não remove as novas linhas, pois gera o texto linha por linha novamente. O segundo comando é apenas uma solução alternativa. Eu acho que sednão é a ferramenta adequada neste caso.
Matt Bianco

Respostas:

14

Isso realmente é trivial no Perl, você não deve odiar!

perl -i.bak -pe 's/>\n/>/' file

Explicação

  • -i: edite o arquivo no local e crie um backup do original chamado file.bak. Se você não quiser um backup, use-o perl -i -pe.
  • -pe: leia o arquivo de entrada linha por linha e imprima cada linha após aplicar o script fornecido como -e.
  • s/>\n/>/: a substituição, assim como sed.

E aqui está uma awkabordagem:

awk  '{if(/>$/){printf "%s",$0}else{print}}' file2 
terdon
fonte
3
+1. awk golf:awk '{ORS=/>$/?"":"\n"}1'
glenn jackman
11
Por que eu não gosto de perl em geral é a mesma razão pela qual escolhi esta resposta (ou, na verdade, seu comentário à resposta de Gnouc): legibilidade. Usar perl -pe com um "padrão sed" simples é muito mais legível do que uma expressão sed complexa.
Matt Bianco
3
@MattBianco é justo o suficiente, mas, para você saber, isso não tem nada a ver com Perl. O aspecto por trás do Gnouc usado é um recurso de algumas linguagens de expressão regular (incluindo, mas não limitado a, PCREs), não é culpa do Perl. Além disso, depois de apresentar essa monstruosidade sedutora ':a;N;$!ba;s/>\n/>/g'em sua pergunta, você renunciou ao seu direito de reclamar sobre legibilidade! : P
terdon
@glennjackman nice! Eu estava brincando com a foo ? bar : bazconstrução, mas não consegui fazê-la funcionar.
terdon
@terdon: Sim, meu erro. Delete isso.
cuonglm
7

Uma perlsolução:

$ perl -pe 's/(?<=>)\n//'

Explicação

  • s/// é usado para substituição de string.
  • (?<=>) é lookbehind padrão.
  • \n corresponde à nova linha.

Todo o padrão significa remover todas as novas linhas que têm >antes.

cuonglm
fonte
2
gostaria de comentar o que as partes do programa fazem? Estou sempre procurando aprender.
Matt Bianco
2
Por que se preocupar com o que está por trás? Por que não apenas s/>\n/>/?
terdon
11
ou s/>\K\n//também funcionaria
glenn jackman
@terdon: Apenas a primeira coisa que
retiro
@glennjackman: bom ponto!
cuonglm
3

Que tal agora:

sed ':loop
  />$/ { N
    s/\n//
    b loop
  }' file

Para o GNU sed, você também pode tentar adicionar a opção -u( --unbuffered) conforme a pergunta. O GNU sed também está satisfeito com isso como uma linha simples:

sed ':loop />$/ { N; s/\n//; b loop }' file
Graeme
fonte
Isso não remove o último \nse o arquivo terminar >\n, mas provavelmente é preferível de qualquer maneira.
Stéphane Chazelas
@ StéphaneChazelas, por que o fechamento }precisa estar em uma expressão separada? isso não funcionará como uma expressão multilinha?
Graeme
11
Isso funcionará em seds POSIX com b loop\n}ou -e 'b loop' -e '}'mas não como b loop;}e certamente não como e b loop}porque são válidos em nomes de rótulos (embora ninguém em sã consciência o usasse. E isso significa que o GNU sed não é compatível com POSIX) e o comando precisa ser separado do comando. };}b
Stéphane Chazelas
@ StéphaneChazelas, o GNU sedestá feliz com todos os itens acima, mesmo com --posix! O padrão também possui o seguinte para expressões entre chaves - The list of sed functions shall be surrounded by braces and separated by <newline>s. Isso não significa que ponto e vírgula deve ser usado apenas fora do aparelho?
Graeme
@mikeserv, o loop é necessário para lidar com linhas consecutivas terminadas em >. O original nunca teve um, isso foi apontado por Stéphane.
Graeme
1

Você deve poder usar sedcom o Ncomando, mas o truque será excluir uma linha do espaço do padrão cada vez que adicionar outra (para que o espaço do padrão sempre contenha apenas 2 linhas consecutivas, em vez de tentar ler a totalidade arquivo) - tente

sed ':a;$!N;s/>\n/>/;P;D;ba'

EDIT: depois de reler Famous Sed One-Liners de Peteris Krumins Explained Eu acredito que uma sedsolução melhor seria

sed -e :a -e '/>$/N; s/\n//; ta'

que anexa apenas a linha a seguir no caso de já ter feito uma >correspondência no final e deve retornar condicionalmente para lidar com o caso de linhas correspondentes consecutivas (é o número 39 de Krumin) . Anexe uma linha à próxima se terminar com uma barra invertida "\" exatamente, exceto a substituição de >por \como o caractere de junção e o fato de que o caractere de junção é retido na saída).

chave de aço
fonte
2
Isso não funciona se 2 linhas consecutivas terminar em >(que também é GNU específicos)
Stéphane Chazelas
1

sednão fornece uma maneira de emitir saída sem uma nova linha final. Sua abordagem usando Nfundamentalmente funciona, mas armazena linhas incompletas na memória e, portanto, pode falhar se as linhas ficarem muito longas (implantações de sed geralmente não são projetadas para lidar com linhas extremamente longas).

Você pode usar o awk.

awk '{if (/<$/) printf "%s", $0; else print}'

Uma abordagem alternativa é usar trpara trocar o caractere de nova linha por um caractere "chato" e que ocorre com frequência. O espaço pode funcionar aqui - escolha um caractere que tende a aparecer em todas as linhas ou pelo menos em uma grande proporção de linhas nos seus dados.

tr ' \n' '\n ' | sed 's/> />/g' | tr '\n ' ' \n'
Gilles 'SO- parar de ser mau'
fonte
Ambos os métodos já foram demonstrados aqui para obter melhores resultados em outras respostas. E sua abordagem com sednão funciona sem um buffer de 2,5 gigabytes.
mikeserv
Alguém mencionou awk? Ah, eu perdi, só notei perl na resposta de Terdon por algum motivo. Ninguém mencionou a trabordagem - mikeserv, você publicou uma abordagem diferente (válida, mas menos genérica) que também usa tr.
Gilles 'SO- stop be evil'
sons válidos, mas menos genéricos para mim, como se você tivesse acabado de chamá-lo de uma solução direcionada e funcional. Eu acho que é difícil argumentar que tal coisa não é útil, o que é estranho porque tem 0 votos a favor. A maior diferença que vejo entre minha própria solução e sua oferta mais genérica é que a minha resolve especificamente um problema, enquanto a sua geralmente pode. Isso pode valer a pena - e posso até reverter meu voto -, mas também há a questão incômoda das 7 horas entre eles e o tema recorrente de suas respostas, imitando outras. Você pode explicar isso?
mikeserv
1

que tal usar ed?

ed -s test.txt <<< $'/fruits/s/apple/banana/g\nw'

(via http://wiki.bash-hackers.org/howto/edit-ed )

andrej
fonte
editado, não há mais dependência no site
andrej
-1

Existem várias maneiras de fazer isso, e a maioria aqui é realmente boa, mas acho que essa é a minha favorita:

tr '>\n' '\n>' | sed 's/^>*//;H;/./!d;x;y/\n>/>\n/'

Ou até:

tr '>\n' '\n>' | sed 's/^>*//' | tr '\n>' '>\n'
mikeserv
fonte
Não consigo fazer sua primeira resposta funcionar. Enquanto admiro a elegância do segundo, acredito que você precisa remover o *. Do jeito que está agora, ele excluirá todas as linhas em branco após uma linha que termine com a >. … Hmm. Olhando para a pergunta, vejo que é um pouco ambígua. A pergunta diz: “Eu quero remover todas as novas linhas que ocorrem depois de um >...” Eu interpreto que isso significa que >\n\n\n\n\nfoodeve ser alterado para \n\n\n\nfoo, mas acho que foopode ser a saída desejada.
Scott
@ Scott - Eu testei com variações no seguinte: printf '>\n>\n\n>>\n>\n>>>\n>\nf\n\nff\n>\n' | tr '>\n' '\n>' | sed 's/^>*//;H;/./!d;x;y/\n>/>\n/'- que resulta em >>>>>>>>>>f\n\nff\n\nmim com a primeira resposta. No entanto, estou curioso para saber o que você está fazendo para quebrá-lo, porque gostaria de corrigi-lo. Quanto ao segundo ponto - não concordo que seja ambíguo. O OP não pede para remover todos > anterior um \newline, mas em vez de remover todas as \n ewlines seguintes um >.
mikeserv
11
Sim, mas uma interpretação válida é que, em >\n\n\n\n\n, somente a primeira nova linha é após a >; todos os outros estão seguindo outras novas linhas. Observe que a sugestão do OP "é isso que eu quero, se funcionasse" sed -e 's/>\n/>/g'não era sed -e 's/>\n*/>/g'.
Scott
11
@ Scott - a sugestão não funcionou e nunca pôde. Não acredito que a sugestão de código de alguém que não entenda completamente o código possa ser considerada um ponto de interpretação válido como a linguagem simples que essa pessoa também usa. E, além disso, a saída - se ele realmente funcionou - de s/>\n/>/em >\n\n\n\n\nainda seria algo que s/>\n/>/editaria.
mikeserv