Como excluir linhas duplicadas em um arquivo sem classificá-lo no Unix?

136

Existe uma maneira de excluir linhas duplicadas em um arquivo no Unix?

Eu posso fazer isso com sort -ue uniqcomandos, mas quero usar sedou awk. Isso é possível?

Vijay
fonte
11
se você quer dizer duplicatas consecutivas, uniqsó isso é suficiente.
Michael Krelin - hacker 18/09/09
caso contrário, acredito que seja possível com awk, mas consumirá bastante recursos em arquivos maiores.
Michael Krelin - hacker
As duplicatas stackoverflow.com/q/24324350 e stackoverflow.com/q/11532157 têm respostas interessantes que devem idealmente ser migradas aqui.
Tripleee

Respostas:

290
awk '!seen[$0]++' file.txt

seené uma matriz associativa para a qual o Awk passará todas as linhas do arquivo. Se uma linha não estiver na matriz, ela seen[$0]será avaliada como falsa. O !operador NOT é lógico e inverte o falso para verdadeiro. O Awk imprimirá as linhas em que a expressão é avaliada como verdadeira. Os ++incrementos seenpara que, seen[$0] == 1após a primeira vez que uma linha seja encontrada seen[$0] == 2, e assim por diante.
O Awk avalia tudo menos 0e ""(string vazia) como true. Se uma linha duplicada for inserida, seenela !seen[$0]será avaliada como falsa e a linha não será gravada na saída.

Jonas Elfström
fonte
5
Para salvá-lo em um arquivo que pode fazer issoawk '!seen[$0]++' merge_all.txt > output.txt
Akash Kandpal
5
Uma ressalva importante aqui: se você precisar fazer isso para vários arquivos, e juntar mais arquivos no final do comando, ou usar um curinga ... a matriz 'vista' será preenchida com linhas duplicadas de TODOS os arquivos. Se você em vez disso quer tratar cada arquivo de forma independente, você precisa fazer algo comofor f in *.txt; do gawk -i inplace '!seen[$0]++' "$f"; done
Nick K9
@ NickK9, que a duplicação cumulativa de vários arquivos é impressionante por si só. Dica agradável
sfscs 14/01
31

Em http://sed.sourceforge.net/sed1line.txt : (Por favor, não me pergunte como isso funciona ;-))

 # delete duplicate, consecutive lines from a file (emulates "uniq").
 # First line in a set of duplicate lines is kept, rest are deleted.
 sed '$!N; /^\(.*\)\n\1$/!P; D'

 # delete duplicate, nonconsecutive lines from a file. Beware not to
 # overflow the buffer size of the hold space, or else use GNU sed.
 sed -n 'G; s/\n/&&/; /^\([ -~]*\n\).*\n\1/d; s/\n//; h; P'
Andre Miller
fonte
geekery ;-) +1, mas o consumo de recursos é inevitável.
Michael Krelin - hacker 18/09/09
3
'$! N; /^(.*)\n\1$/!P; D 'significa "Se você não estiver na última linha, leia em outra linha. Agora, veja o que você tem e, se não for seguido por uma nova linha e, em seguida, o mesmo material novamente, imprima o material. Agora exclua o material (até a nova linha). "
Beta
2
'G; s / \ n / && /; / ^ ([- ~] * \ n). * \ n \ 1 / d; s / \ n //; h; P 'significa, grosso modo, "Anexar todo o espaço de espera nesta linha; se você ver uma linha duplicada jogar tudo fora, copie toda a bagunça de volta para o espaço de espera e imprima a primeira parte (que é a linha que você acabou de leia. "
Beta
A $!peça é necessária? Não sed 'N; /^\(.*\)\n\1$/!P; D'faz a mesma coisa? Não consigo criar um exemplo em que os dois sejam diferentes na minha máquina (fwiw, tentei uma linha vazia no final com as duas versões e ambas estavam bem).
28612 eddi
1
Quase 7 anos depois e ninguém respondeu @amichair ... <sniff> me deixa triste. ;) Enfim, [ -~]representa um intervalo de caracteres ASCII de 0x20 (espaço) a 0x7E (til). Esses são considerados os caracteres ASCII imprimíveis (a página vinculada também possui 0x7F / delete, mas isso não parece correto). Isso faz com que a solução seja quebrada para quem não usa ASCII ou para quem digita, por exemplo, caracteres de tabulação. O mais portátil [^\n]inclui muito mais caracteres ... todos, exceto um, de fato.
Camada B,
14

One-liner Perl semelhante à solução awk de @ jonas:

perl -ne 'print if ! $x{$_}++' file

Essa variação remove o espaço em branco à direita antes de comparar:

perl -lne 's/\s*$//; print if ! $x{$_}++' file

Essa variação edita o arquivo no local:

perl -i -ne 'print if ! $x{$_}++' file

Essa variação edita o arquivo no local e faz um backup file.bak

perl -i.bak -ne 'print if ! $x{$_}++' file
Chris Koknat
fonte
6

A linha única que Andre Miller postou acima funciona, exceto nas versões recentes do sed quando o arquivo de entrada termina com uma linha em branco e sem caracteres. No meu Mac, minha CPU gira.

Loop infinito se a última linha estiver em branco e não tiver caracteres :

sed '$!N; /^\(.*\)\n\1$/!P; D'

Não trava, mas você perde a última linha

sed '$d;N; /^\(.*\)\n\1$/!P; D'

A explicação está no final da FAQ sed :

O mantenedor do GNU sed considerou que, apesar dos problemas de portabilidade que
isso causaria, alterar o comando N para imprimir (em vez de
excluir) o espaço do padrão era mais consistente com as intuições de alguém
sobre como deveria se comportar um comando para "acrescentar a próxima linha" .
Outro fato favorável à mudança foi que "{N; command;}"
excluirá a última linha se o arquivo tiver um número ímpar de linhas, mas
imprimirá a última linha se o arquivo tiver um número par de linhas.

Para converter scripts que usavam o antigo comportamento de N (excluindo
o espaço do padrão ao atingir o EOF) em scripts compatíveis com
todas as versões do sed, altere um "N" isolado; para "$ d; N;" .

Bradley Kreider
fonte
5

Uma maneira alternativa de usar o Vim (compatível com Vi) :

Exclua linhas duplicadas e consecutivas de um arquivo:

vim -esu NONE +'g/\v^(.*)\n\1$/d' +wq

Exclua linhas duplicadas, não consecutivas e não vazias de um arquivo:

vim -esu NONE +'g/\v^(.+)$\_.{-}^\1$/d' +wq

Bohr
fonte
4

A primeira solução também é de http://sed.sourceforge.net/sed1line.txt

$ echo -e '1\n2\n2\n3\n3\n3\n4\n4\n4\n4\n5' |sed -nr '$!N;/^(.*)\n\1$/!P;D'
1
2
3
4
5

a ideia central é:

print ONLY once of each duplicate consecutive lines at its LAST appearance and use D command to implement LOOP.

Explica:

  1. $!N;: se a linha atual NÃO for a última, use N comando para ler a próxima linha pattern space.
  2. /^(.*)\n\1$/!P: se o conteúdo da corrente pattern spaceé dois duplicate stringseparados por \n, o que significa que a próxima linha é a samelinha com corrente, NÃO podemos imprimi-lo de acordo com nossa idéia principal; caso contrário, o que significa que a linha atual é a ÚLTIMA aparência de todas as suas linhas consecutivas duplicadas, agora podemos usar o Pcomando para imprimir os caracteres no pattern spaceutilitário atual \n(\n também impresso).
  3. D: usamos o Dcomando para excluir os caracteres no pattern spaceutilitário atual \n(\n também excluído), e o conteúdo de pattern spaceé a próxima linha.
  4. e Dcomando forçará seda pular para seu FIRSTcomando$!N , mas NÃO lê a próxima linha do arquivo ou fluxo de entrada padrão.

A segunda solução é fácil de entender (por mim):

$ echo -e '1\n2\n2\n3\n3\n3\n4\n4\n4\n4\n5' |sed -nr 'p;:loop;$!N;s/^(.*)\n\1$/\1/;tloop;D'
1
2
3
4
5

a ideia central é:

print ONLY once of each duplicate consecutive lines at its FIRST appearance and use : command & t command to implement LOOP.

Explica:

  1. leia uma nova linha do fluxo ou arquivo de entrada e imprima-a uma vez.
  2. use o :loopcomando set a labelnamed loop.
  3. use Npara ler a próxima linha no pattern space.
  4. use s/^(.*)\n\1$/\1/para excluir a linha atual se a próxima linha for a mesma da linha atual, usamos o scomando para deleteexecutar a ação.
  5. se o scomando for executado com sucesso, use a tloopforça de comando sedpara pular para o labelnomeado loop, que fará o mesmo loop para as próximas linhas util; não há linhas consecutivas duplicadas da linha que é latest printed; caso contrário, use o Dcomando para deletea linha que é a mesma com o latest-printed linee force sedpara pular para o primeiro comando, que é o pcomando, o conteúdo de current pattern spaceé a próxima nova linha.
Weike
fonte
mesmo comando no Windows com o busybox:busybox echo -e "1\n2\n2\n3\n3\n3\n4\n4\n4\n4\n5" | busybox sed -nr "$!N;/^(.*)\n\1$/!P;D"
limpador
-1

Isso pode ser alcançado usando awk. A
linha abaixo exibirá valores exclusivos

awk file_name | uniq

Você pode gerar esses valores exclusivos para um novo arquivo

awk file_name | uniq > uniq_file_name

o novo arquivo uniq_file_name conterá apenas valores exclusivos, sem duplicatas

Aashutosh
fonte
-4
cat filename | sort | uniq -c | awk -F" " '$1<2 {print $2}'

Exclui as linhas duplicadas usando o awk.

Sadhun
fonte
1
Isso irá perturbar a ordem das linhas.
Vijay
1
O que é um arquivo de texto de 20 GB? Muito devagar.
11277 Alexander
Como sempre, o caté inútil. De qualquer forma, uniqjá faz isso por si só e não exige que a entrada seja exatamente uma palavra por linha.
Tripleee