Remove entradas repetitivas de um arquivo

1

Existe um arquivo com algumas entradas idênticas e repetitivas, algo parecido com isto:

123 abc nhjk
123 abc cftr
123 abc xdrt
123 def nhjk
123 def cftr
123 def xdrt

Se a combinação de (coloumns) Field1 e Field2 corresponder, então apenas a primeira ocorrência em que ambos coincidirem precisa ser mantida. Então, como 123 e abc da primeira linha correspondem a 123 e abc da segunda linha, somente o primeiro a linha deve ser mantida. Em uma comparação mais profunda, já que a correspondência é verdadeira para a primeira e terceira linha também, novamente apenas a primeira linha deve ser mantida.

No entanto, Para a primeira e quarta linha, 123 e 123 correspondem, mas abc e def não coincidem, então ambas as linhas são para ser retido.

O resultado final deve ser assim:

123 abc nhjk
123 def nhjk
    
por Cartwig 24.10.2015 / 08:09

5 respostas

3

Uma maneira de fazer isso é com o -u flag para sort , embora isso possa não preservar a ordem original dos arquivos:

sort -k1,1 -k2,2 -u file

Se você precisar fazer uma desduplicação com a ordem dos arquivos preservada

awk '!a[$1, $2]++' file
    
por 24.10.2015 / 08:14
2

Respostas incríveis de RobertL e 1_CR

Se você preferir uma abordagem de shell script mais flexível, tente o seguinte script:

#!/bin/sh

rm output.txt
touch output.txt
while read line
do
    field1=$( echo $line | cut -d" " -f1)
    field2=$( echo $line | cut -d" " -f2)
    lookup="$field1 $field2"
    if  [ -z $(grep "$lookup" output.txt) ]
    then
        echo $line >> output.txt
    fi
done < input.txt
cat output.txt
exit 0

Obviamente, pode ser encurtado muito, mas eu queria fazer cada passo muito claro.

Aproveite.

EDITAR:

Seguindo o link postado pelo @RobertL e depois de testar várias opções, eu tenho que concordar que há uma grande melhoria neste script. Eu usaria

#!/bin/sh

sort -k1,2 -u "$@" |
while read line
do
     echo "$line"
done

Minha única pergunta sobre isso é RobertL mas é por que usar:

sort -k1,2 -k2,2 -u

em vez de

sort -k1,2 -u

De acordo com o meu próprio teste, o seu tipo é eficiente,

$ cat robertL.sh
    #!/bin/sh

    sort -k1,1 -k2,2 -u "$@" |
    while read line
    do
         echo "$line"
    done

$ time ./robertL.sh < input.txt

123 abc nhjk
123 def nhjk

real    0m0.022s
user    0m0.014s
sys     0m0.009s

Mas o outro é duas vezes mais rápido,

$ cat process_v2.sh
#!/bin/sh

sort -k1,2 -u "$@" |
while read line
do
     echo "$line"
done

$ time ./process_v2.sh < input.txt

123 abc nhjk
123 def nhjk

real    0m0.012s
user    0m0.006s
sys     0m0.009s

Assim, como conclusão, é altamente recomendável a abordagem de RobertL, mas sempre leve tudo aqui como uma amostra, e não como uma verdade absoluta ou uma solução final para sua pergunta. Eu acho que o melhor aqui é encontrar orientação através de respostas.

    
por 24.10.2015 / 19:13
1

Se você precisar processar intensamente cada registro da saída, você pode criar um filtro que leia cada linha da saída. Não processe os registros dentro do algoritmo class / unique.

O script original leva cerca de 1 segundo para processar cada 100 registros. O script que lê a saída do tipo levou menos de 3/10 de segundo para processar mais de 380.000 registros. Teria sido necessário o script original mais de uma hora para processar essa quantidade de dados.

Uma hora em comparação com 3/10 de um segundo!

Observe também que o script original também passou a maior parte do tempo na hora do sistema (processos de bifurcação, fazendo io, etc.), outro sinal ruim de problemas de desempenho.

Execução do script original:

    $ wc -l input.txt 
    1536 input.txt
    $ time ./jesus.sh
    rm: cannot remove ‘output.txt’: No such file or directory
    123 abc nhjk
    123 def nhjk

    real    0m16.997s              #<<<---------
    user    0m3.546s
    sys 0m16.329s                  #<<<---------

Execução deste novo script de exemplo, apenas uma pequena fração do tempo de execução é gasta no código do sistema operacional:

    $ time ./RobertL.sh < input.txt
    123 abc nhjk
    123 def nhjk        

    real    0m0.011s               #<<<---------
    user    0m0.004s
    sys 0m0.007s                   #<<<---------

Agora, executamos o novo script em um conjunto de dados enorme que sabemos que levaria o script original por mais de uma hora para ser concluído:

    $ wc -l data388440.txt 
    388440 data388440.txt
    $ time ./RobertL.sh < data388440.txt 
    123 abc nhjk
    123 def nhjk        

    real    0m0.282s               #<<<---------
    user    0m0.728s
    sys 0m0.032s                   #<<<---------

O novo script de exemplo:

    $ cat RobertL.sh
    #!/bin/sh

    sort -k1,1 -k2,2 -u "$@" |
    while read line
    do
         echo "$line"
    done

O script original, modificado para ser executado sem instalar o ksh:

    $ cat jesus.sh
    #!/bin/bash
    #!/bin/sh  # does not accept [[ ... ]]
    #!/bin/ksh # not installed on ubuntu by default

    rm output.txt
    touch output.txt
    while read line
    do
        field1=$( echo $line | cut -d" " -f1)
        field2=$( echo $line | cut -d" " -f2)
        lookup="$field1 $field2"
        if  [[ -z $(grep "$lookup" output.txt) ]]
        then
            echo $line >> output.txt
        fi
    done < input.txt
    cat output.txt
    exit 0

Os dados de entrada foram criados repetindo as 6 linhas originais de dados de amostra, os dados continham quase todos os registros duplicados.

    
por 25.10.2015 / 19:36
1

Que tal este sed one-liner:

sed -n '${;p;q;};N;/^ *\([^ ][^ ]*  *[^ ][^ ]*\)\( .*\)*\n */{;s/\n.*//;h;G;D;};P;D' inputfile

Este foi um bom desafio complicado; obrigado! :)

Em um nível alto, o que isso faz é percorrer o arquivo de entrada comparando duas linhas de cada vez. Se as linhas coincidirem com as duas primeiras palavras, a linha segunda das duas será descartada e a linha próxima do arquivo será usada para comparar com a primeira linha. Se as linhas não coincidirem, a primeira será impressa e a segunda retida para comparação com as linhas posteriores. Quando o final do arquivo é atingido, a linha atualmente "mantida para comparação" é impressa.

Explicação de golpe a golpe:

-n  doN't print lines by default; only if specified to print them.

${;p;q;};   if on the la$t line then Print the line and Quit.
N;  append a newline followed by the Next line of the file to the pattern space
/^ *\([^ ][^ ]*  *[^ ][^ ]*\)\( .*\)*\n */    A very tricky regex:
    match any leading spaces, followed by a nonspace sequence, space or
    multiple spaces, nonspace sequence, then optionally a space followed
    by anything, then a newline, then any leading spaces, then the matched
    two words from earlier again.
{;  if that regex matched the pattern space, excecute the following.
s/\n.*//;   delete the first newline and everything after it
h;  copy the pattern space contents to the Hold space
G;  append (Get) a newline followed by the hold space contents to the pattern space
D;  delete everything in the pattern space up to the first newline, then start from the beginning of this sequence (with the ${ block)
};  end of block.  Skip to here if the tricky regex didn't match.
P;  Print everything in the pattern space up to the first newline.
D   Delete the pattern space up to the first newline.

Observe que o acima é muito portátil. Deliberadamente. Só por um desafio eu queria que ele fosse executado sem ? ou + estar disponível (já que eles não são compatíveis com POSIX), o que torna a regex muito mais complicada.

Além disso, o fluxo lógico não inclui nenhuma ramificação, embora as ramificações sejam compatíveis com POSIX e estejam disponíveis universalmente. Por que eu fiz isso? É porque nem todas as implementações de sed permitem que rótulos sejam especificados em um único liner. Eles exigem um \ e uma nova linha após o rótulo. O GNU sed permite rótulos em um one-liner e, por exemplo, o BSD sed não.

Os dois liners a seguir são exatamente iguais usando o GNU sed, a única diferença é que eles são mais robustos ao lidar com guias e espaços:

sed -n ':k;${;p;q;};N;/^\s*\(\S\+\s\+\S\+\)\(\s.*\)\?\n\s*/{;s/\n.*//;bk;};P;D' inputfile
sed -n ':k;${;p;q;};N;s/^\(\s*\(\S\+\s\+\S\+\)\(\s.*\)\?\)\n\s*.*$//;tk;P;D' inputfile

Eu principalmente fiz isso por diversão. :) Eu acho que a resposta do 1_CR é a melhor, e é claro que é muito mais simples.

Se seus requisitos ficarem um pouco mais complicados do que são atualmente e a abordagem dele não funcionar, a melhor ferramenta provavelmente será awk . Mas ainda não aprendi awk e aprendi sed . :)

    
por 26.10.2015 / 06:51
0

Se as linhas a serem removidas forem todas consecutivas e as chaves tiverem o mesmo comprimento, você poderá usar:

$ uniq --check-chars=8 <<EOF
123 abc nhjk
123 abc cftr
123 abc xdrt        
123 def nhjk        
123 def cftr        
123 def xdrt
EOF         
123 abc nhjk
123 def nhjk
$
    
por 24.10.2015 / 08:48