Como remover linhas duplicadas dentro de um arquivo de texto?

120

Um arquivo de texto enorme (até 2 GiB) meu contém cerca de 100 duplicatas exatas de cada linha nele (inútil no meu caso, já que o arquivo é uma tabela de dados do tipo CSV).

O que eu preciso é remover todas as repetições enquanto (de preferência, mas isso pode ser sacrificado por um aumento significativo no desempenho) mantendo a ordem original da sequência. No resultado, cada linha deve ser única. Se houver 100 linhas iguais (geralmente as duplicatas estão espalhadas pelo arquivo e não serão vizinhas), haverá apenas uma do tipo restante.

Eu escrevi um programa no Scala (considere Java se você não sabe sobre o Scala) para implementar isso. Mas talvez haja ferramentas nativas mais rápidas, escritas em C, capazes de fazer isso mais rápido?

UPDATE: a solução awk '!seen[$0]++' filename parecia estar funcionando bem para mim, desde que os arquivos estivessem perto de 2 GiB ou menores, mas agora, como estou limpando um arquivo de 8 GiB, ele não funciona mais. Parece tomar infinito em um Mac com 4 GiB de RAM e um Windows 7 de 64 bits com 4 GiB de RAM e 6 GiB swap apenas fica sem memória. E eu não me sinto entusiasmado em testá-lo no Linux com 4 GiB de RAM, dada essa experiência.

    
por Ivan 27.01.2012 / 16:34

9 respostas

203

Uma solução awk vista em #bash (Freenode):

awk '!seen[$0]++' filename
    
por 27.01.2012 / 17:18
45

Existe um método simples (que não é óbvio) usando utilitários padrão que não requerem uma grande memória, exceto executar sort , que na maioria das implementações possui otimizações específicas para arquivos grandes (um bom algoritmo de ordenação externa) . Uma vantagem deste método é que ele apenas faz um loop em todas as linhas dentro de utilitários de propósito especial, nunca dentro de linguagens interpretadas.

<input nl -b a -s : |           # number the lines
sort -t : -k 2 -u |             # sort and uniquify ignoring the line numbers
sort -t : -k 1n |               # sort according to the line numbers
cut -d : -f 2- >output          # remove the line numbers

Se todas as linhas começarem com um caractere sem espaço em branco, você poderá dispensar algumas das opções:

<input nl | sort -k 2 -u | sort -k 1n | cut -f 2- >output

Para uma grande quantidade de duplicação, um método que requer apenas o armazenamento de uma única cópia de cada linha na memória terá um melhor desempenho. Com alguma sobrecarga de interpretação, há um script awk muito conciso para isso (já postado por enzotib ):

<input awk '!seen[$0]++'

Menos concisamente: !seen[$0] {print} {seen[$0] += 1} , isto é, imprime a linha atual se ela ainda não foi vista, e então incrementa o contador seen desta linha (variáveis não inicializadas ou elementos de array têm o valor numérico 0).

Para linhas longas, você pode economizar memória mantendo apenas uma soma de verificação não falsificável (por exemplo, um resumo criptográfico) de cada linha. Por exemplo, usando SHA-1, você só precisa de 20 bytes mais uma sobrecarga constante por linha. Mas a computação digere é bastante lenta; este método só ganhará se você tiver uma CPU rápida (especialmente uma com um acelerador de hardware para computar as compilações) e não muita memória relativa ao tamanho do arquivo e linhas suficientemente longas. Nenhum utilitário básico permite calcular uma soma de verificação para cada linha; você teria que suportar a sobrecarga de interpretação de Perl / Python / Ruby /… ou escrever um programa compilado dedicado.

<input perl -MDigest::MD5 -ne '$seen{Digest::MD5::md5($_)}++ or print' >output
    
por 27.01.2012 / 18:19
22
sort -u big-csv-file.csv > duplicates-removed.csv

Observe que o arquivo de saída será classificado.

    
por 13.03.2015 / 23:57
18

Supondo que você pode manter tanto arquivo duplicado na memória (se seus dados forem de fato duplicados por um fator de 100, isso deve ser cerca de 20MiB +), você pode fazer isso facilmente com Perl.

$ perl -ne 'print unless $dup{$_}++;' input_file > output_file

Isso preserva a ordem também.

Você pode extrair o número de ocorrências de cada linha do hash %dup , se desejar, como um bônus adicional.

Se você preferir awk , isso também deve ser feito (mesma lógica da versão perl, mesma ordem, mesmos dados reunidos na variável dup ):

$ awk '{if (++dup[$0] == 1) print $0;}' input_file > output_file
    
por 27.01.2012 / 17:06
3

Como nenhuma outra resposta forneceu suporte, aqui está um:

gawk -i inplace '!a[$0]++' file
    
por 31.10.2016 / 19:13
2

Você pode usar o link uniq

uniq informa ou filtra linhas repetidas em um arquivo.

    
por 24.03.2017 / 05:02
1

Liners do Python One:

python -c "import sys; lines = sys.stdin.readlines(); print ''.join(sorted(set(lines)))" < InputFile
    
por 15.09.2013 / 11:13
0

Nenhuma das respostas aqui funcionou para mim no meu Mac, então escrevi um script Python simples que funciona para mim. Eu estou ignorando o espaço em branco inicial / final e também não me importo com o consumo de memória.

import sys

inputfile = sys.argv[1]
outputfile = sys.argv[2]

with open(inputfile) as f:
    content = f.readlines()

content = [x.strip() for x in content]

my_list = list(set(content))

with open(outputfile, 'w') as output:
    for item in my_list:
        output.write("%s\n" % item)

Salve o acima em unique.py e corra assim:

python unique.py inputfile.txt outputfile.txt
    
por 02.03.2018 / 17:14
-1

Com o bash 4, uma solução pura que tira vantagem de arrays associativos pode ser usado. Aqui está um exemplo

unset llist; declare -A llist;
while read -r line; do
if [[ ${llist[$line]} ]]; then
  continue
else 
  printf '%s\n' "$line"
  llist[$line]="x"
fi
done < file.txt
    
por 16.09.2013 / 22:49