Jak usunąć zduplikowane linie w pliku tekstowym?

126

Mój ogromny plik tekstowy (do 2 GiB) zawiera około 100 dokładnych duplikatów każdego wiersza w nim (w moim przypadku jest to bezużyteczne, ponieważ jest to tabela danych podobna do CSV).

To, czego potrzebuję, to usunięcie wszystkich powtórzeń, podczas gdy (najlepiej, ale można to poświęcić w celu znacznego zwiększenia wydajności) przy zachowaniu oryginalnej kolejności sekwencji. W rezultacie każda linia ma być unikalna. Jeśli było 100 równych wierszy (zwykle duplikaty są rozłożone w pliku i nie będą sąsiadami), pozostanie tylko jeden taki rodzaj.

Napisałem program w Scali (rozważ to Java, jeśli nie wiesz o Scali), aby to zaimplementować. Ale może są szybsze natywne narzędzia napisane w C, które mogą to zrobić szybciej?

AKTUALIZACJA: awk '!seen[$0]++' filenamerozwiązanie wydawało się działać dobrze dla mnie, dopóki pliki były w pobliżu 2 GiB lub mniejszych, ale teraz, gdy mam wyczyścić plik 8 GiB, to już nie działa. Wydaje się, że zabiera nieskończoność na komputerze Mac z 4 GiB RAM i 64-bitowym Windows 7 PC z 4 GiB RAM i 6 GiB swap po prostu kończy się pamięć. I nie czuję entuzjazmu, próbując tego na Linuksie z 4 GiB RAM biorąc pod uwagę to doświadczenie.

Ivan
źródło
to zniszczy twoje zamówienie, ale czy próbowałeś sortować -u, nie mam pojęcia, jak i czy można go uruchomić na tak ogromnym pliku
0x7c0
5
C często nie jest znacznie szybszy niż Java, a jeśli teraz go uruchamiasz (w kolejności), istnieje spora szansa, że ​​skończy, zanim pojawi się tutaj odpowiedź, zaimplementuj ją i zakończy działanie; zepsuty, sort -uprawdopodobnie będzie szybszy.
Kevin

Odpowiedzi:

214

awkRozwiązanie widoczne na #bash (Freenode):

awk '!seen[$0]++' filename
enzotib
źródło
1
Właśnie wypróbowałem to na pliku 2G i zajęło mi to trzy minuty na moim notebooku. Nie jest zły. Próbowałem także nazwy pliku uniq | awk '! seen [$ 0] ++', ale nie było to szybsze.
mgjk
Jest to zaskakująco szybsze niż bardziej pełna awkwersja z użyciem 2 wyszukiwań tablicowych (pokazanych jako wyjaśnienie w odpowiedzi Gillesa): 0m36.132s vs 0m49.958s .. dla 50 milionów linii .. Myślałem, że wąskim gardłem będzie I / O, ale dodatkowe wyszukiwanie tablic to ... milion elementów w tablicy wydaje się robić znaczące wgniecenie ...
Peter.O
Ale jak to się ma do sort -u ....?
HashWizard
1
@HashWizard: to polecenie nie sortuje, ale eliminuje każde kolejne wystąpienie tej samej linii
enzotib
1
@MaxWilliams tak, działa, jeśli są losowo dystrybuowane.
setholopolus,
47

Istnieje prosta (co nie jest oczywiste) metoda wykorzystująca standardowe narzędzia, które nie wymagają dużej pamięci poza uruchomieniem sort, która w większości implementacji ma określone optymalizacje dla dużych plików (dobry algorytm sortowania zewnętrznego). Zaletą tej metody jest to, że zapętla ona tylko wszystkie linie wewnątrz narzędzi specjalnego przeznaczenia, nigdy wewnątrz interpretowanych języków.

<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

Jeśli wszystkie wiersze zaczynają się od spacji, możesz zrezygnować z niektórych opcji:

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

W przypadku dużej ilości duplikacji metoda, która wymaga tylko przechowywania pojedynczej kopii każdej linii w pamięci, działa lepiej. Po pewnym nakładzie interpretacyjnym istnieje bardzo zwięzły skrypt awk (już opublikowany przez enzotib ):

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

Mniej zwięźle: !seen[$0] {print} {seen[$0] += 1}tzn. Wydrukuj bieżącą linię, jeśli jeszcze nie była widoczna, następnie zwiększ seenlicznik dla tej linii (niezainicjowane zmienne lub elementy tablicy mają wartość liczbową 0).

W przypadku długich linii można zaoszczędzić pamięć, przechowując tylko niepodlegającą fałszowaniu sumę kontrolną (np. Streszczenie kryptograficzne) każdej linii. Na przykład, używając SHA-1, potrzebujesz tylko 20 bajtów plus stały narzut na linię. Ale przetwarzanie danych jest raczej powolne; ta metoda wygra tylko wtedy, gdy masz szybki procesor (zwłaszcza taki ze sprzętowym akceleratorem do obliczania skrótów) i nie ma dużo pamięci w stosunku do wielkości pliku i wystarczająco długich linii. Żadne podstawowe narzędzie nie pozwala obliczyć sumy kontrolnej dla każdej linii; będziesz musiał ponieść koszty interpretacji Perla / Pythona / Ruby /… lub napisać dedykowany skompilowany program.

<input perl -MDigest::MD5 -ne '$seen{Digest::MD5::md5($_)}++ or print' >output
Gilles
źródło
@Gilles Czy na podstawie twojego wyjaśnienia awk '!seen[$0]++'oznacza to, że jeśli awk zobaczy 2 zduplikowane linie, zachowa zawsze pierwszą linię i zignoruje wszystkie kolejne? (Czy zachowa ostatni?)
user779159
1
@ user779159 Zachowuje pierwszy: każda linia wejściowa jest albo drukowana natychmiast (pierwsze wystąpienie), albo wcale (powtórzenie).
Gilles
Ale jak to się ma do sort -u ...?
HashWizard
@HashWizard Zwykły sort -uzmienia kolejność. Moja odpowiedź pokazuje rozwiązania, które zachowują porządek (a dokładniej kolejność pierwszych wystąpień).
Gilles
@Gilles, czy powiedziałbyś, że jest szybszy niż sort -u dla dużych plików (10G) z 50% duplikatami?
HashWizard
25
sort -u big-csv-file.csv > duplicates-removed.csv

Zauważ, że plik wyjściowy zostanie posortowany.

Vladislavs Dovgalecs
źródło
1
Nie tak szybkie jak awkpolecenie w innych odpowiedziach, ale koncepcyjnie proste!
Johann
@Johann Robię to dość często na plikach z setkami tysięcy (nawet milionów) krótkich ciągów zakończonych znakiem nowej linii. Otrzymuję wyniki dość szybko dla eksperymentów, które przeprowadzam. Może być ważniejszy, jeśli jest używany w wielokrotnie uruchamianych skryptach, oszczędność czasu może być znaczna.
Vladislavs Dovgalecs
1
Służy sort -udo usuwania duplikatów podczas sortowania, a nie po nim. (I oszczędza przepustowość pamięci) przesyłając go do innego programu). Jest to lepsze niż awkwersja, jeśli chcesz również posortować dane wyjściowe. (OP w tym pytaniu chce zachować jego pierwotne zamówienie , więc jest to dobra odpowiedź na nieco inny przypadek użycia).
Peter Cordes
Zajęło mi to około minuty na plik o długości 5,5 miliona (łącznie 1,8 GB). Znakomity.
Max Williams
18

Zakładając, że możesz sobie pozwolić na zachowanie w pamięci nawet zduplikowanego pliku (jeśli twoje dane są rzeczywiście zduplikowane 100-krotnie, powinno to wynosić około 20MiB + narzut), możesz to zrobić bardzo łatwo za pomocą Perla.

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

To także zachowuje porządek.

Jeśli chcesz, możesz wyodrębnić liczbę wystąpień każdej linii z %dupskrótu, jako dodatkowy bonus.

Jeśli wolisz awk, to też powinno to zrobić (ta sama logika co wersja perla, to samo porządkowanie, te same dane zebrane w dupzmiennej):

$ awk '{if (++dup[$0] == 1) print $0;}' input_file > output_file
Mata
źródło
To jest zbyt dobre @Mat, miałem już zamiatać plik, lol ;-).
Nikhil Mulley,
Teraz czeka na @ManAtWork na jego tkactwo i magię tkania magii :-)
Nikhil Mulley
znowu niesamowity jak na awk wskazówka :-)
Nikhil Mulley
1
Czy można zmienić skrypt perla, aby usunąć tylko zduplikowane sąsiednie linie?
dumbledad
2
@dumbledad: uniqrobi to samo
Mat
3

Ponieważ żadna inna odpowiedź nie zapewnia wsparcia w miejscu, oto jedna z nich:

gawk -i inplace '!a[$0]++' file
Jan Chren - rindeal
źródło
Czy to zachowuje porządek? Nawiasem mówiąc, to mi nie zadziałało. Moja wersja to:GNU Awk 4.0.2
Leonid
1
@Leonid tak, robi. Drukuje pierwsze wystąpienie dowolnej unikalnej linii. Wsparcie w miejscu zostało wprowadzone po raz pierwszy w wersji 4.1, która została wydana w 2013 roku.
Jan Chren - rindeal
3

Możesz użyć uniq http://www.computerhope.com/unix/uuniq.htm

uniq zgłasza lub odfiltrowuje powtarzające się wiersze w pliku.

Mahmoud Zalt
źródło
Udzielając odpowiedzi, lepiej jest wyjaśnić, DLACZEGO twoja odpowiedź jest jedna. Czym zatem różni się ta odpowiedź od kilku poprzednich odpowiedzi?
Stephen Rauch
1
Ze strony podręcznika użytkownika uniq: Uwaga: 'uniq' does not detect repeated lines unless they are adjacent. Najpierw musisz go posortować i stracić kolejność niepowielonych linii.
Vindolin,
2

Wkładki Python One:

python -c "import sys; lines = sys.stdin.readlines(); print ''.join(sorted(set(lines)))" < InputFile
Rahul Patil
źródło
powoduje to, że cały plik jest zawieszony w pamięci i może nie być dobrym rozwiązaniem dla problemu OP. Nie gwarantujemy również utrzymania zamówienia
iruvar 15.09.13
Dzięki za sugestię, właśnie uczyłem się Pythona .. właśnie spróbowałem tego w celu uczenia się .. :)
Rahul Patil
Oto wersja Python 2.7, która nie jest jednowierszowa, ale (zwięźle) zwraca unikatowe wiersze zachowujące porządek bez ładowania całego pliku do pamięci lub tworzenia pojedynczego gigantycznego ciągu do wydrukowania
iruvar 16.09.13
Dzięki @ 1_CR Mam coś się dziś nauczyć :)OrderedDict
Rahul Patil
0

Żadna z odpowiedzi tutaj nie działała na moim komputerze Mac, więc napisałem prosty skrypt Pythona, który działa dla mnie. Ignoruję początkowe / końcowe białe znaki, a także nie obchodzi mnie zużycie pamięci.

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)

Zapisz powyższe w pliku Unique.py i uruchom w ten sposób:

python unique.py inputfile.txt outputfile.txt
Jared
źródło
-1

W przypadku bash 4 można zastosować rozwiązanie typu pure bash, które wykorzystuje tablice asocjacyjne . Oto przykład

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
iruvar
źródło
2
Nie używaj readpętli do przetwarzania dużych plików tekstowych. bash musi czytać jeden bajt na raz, aby uniknąć przekroczenia nowej linii. Bash również nie jest bardzo szybki w przetwarzaniu tekstu w porównaniu do awk. Jeśli tego użyjesz, read -raunikniesz jedzenia odwrotnych ukośników w danych wejściowych. Nie zapomnij też unset llist po zakończeniu pętli, jeśli umieścisz ją w funkcji powłoki lub użyjesz jej interaktywnie.
Peter Cordes,
2
@PeterCordes, lub mógłbyś po prostu wspomnieć o tym :-)
iruvar