Szybki sposób znajdowania linii w jednym pliku, których nie ma w innym?

Mam dwa duże pliki (zestawy nazw plików). Około 30.000 linii w każdym pliku. Staram się szybko znaleźć linie w pliku1, których nie ma w pliku2.

Na przykład, jeśli jest to file1:

line1
line2
line3

A to jest file2:

line1
line4
line5

Wtedy mój wynik / wynik powinien być:

line2
line3

To działa:

grep -v -f file2 file1

Ale jest bardzo, bardzo powolny, gdy jest używany na moich dużych plikach.

Podejrzewam, że istnieje dobry sposób, aby to zrobić za pomocą diff (), ale wyjście powinno być po prostu linie, nic więcej, i nie mogę znaleźć przełącznika do tego.

Czy ktoś może mi pomóc znaleźć szybki sposób na to, używając Basha i podstawowych binariów Linuksa?

EDIT: aby odpowiedzieć na moje własne pytanie, jest to najlepszy sposób, jaki do tej pory znalazłem używając diff ():

diff file2 file1 | grep '^>' | sed 's/^>\ //'
Na pewno musi być lepszy sposób?
Author: Niels2000, 2013-08-13

9 answers

Możesz to osiągnąć kontrolując formatowanie starych/nowych / niezmienionych linii w GNU diff wyjście:

diff --new-line-format="" --unchanged-line-format=""  file1 file2

Pliki wejściowe powinny być posortowane aby to zadziałało. Z bash (i zsh) można sortować w miejscu z substytucją procesu <( ):

diff --new-line-format="" --unchanged-line-format="" <(sort file1) <(sort file2)

W powyższym nowe i niezmienione linie są tłumione, więc tylko zmienione (tzn. usunięte linie w Twoim przypadku) są wyprowadzane. Możesz również użyć kilku opcji diff, które Inne rozwiązania nie oferują, takie jak -i ignorowanie wielkości liter lub różne opcje białych znaków (-E, -b, -v itp.) dla mniej ścisłego dopasowania.


Wyjaśnienie

Opcje --new-line-format, --old-line-format i --unchanged-line-format pozwala kontrolować sposób formatowania różnic, podobny do specyfikacji formatu printf. Opcje te formatują odpowiednio nowe (dodane), stare (usunięte) i niezmienione linie. Ustawienie jedynki na pustą "" uniemożliwia wyjście tego typu linii.

Jeśli znasz format unified diff , możesz częściowo odtworzyć go za pomocą:

diff --old-line-format="-%L" --unchanged-line-format=" %L" \
     --new-line-format="+%L" file1 file2
Na przykład, w przypadku, gdy nie jest to możliwe, w przypadku, gdy nie jest to możliwe, nie jest to możliwe.]} (zauważ, że wyprowadza tylko różnice, brakuje mu --- +++ i @@ linie u góry każdej zmiany zgrupowanej). Możesz również użyć tego do zrobienia innych przydatnych rzeczy, takich jak Numeruj każdą linię Z %dn.

Metoda diff (wraz z inne sugestie comm i join) generują oczekiwane dane wyjściowe tylko z posortowanym wejściem, chociaż możesz użyć <(sort ...) do sortowania w miejscu. Oto prosty skrypt awk (nawk) (zainspirowany skryptami linkowanymi w odpowiedzi Konsolebox), który akceptuje dowolnie uporządkowane pliki wejściowe, i wypisuje brakujące linie w kolejności, w jakiej występują w pliku1.

# output lines in file1 that are not in file2
BEGIN { FS="" }                         # preserve whitespace
(NR==FNR) { ll1[FNR]=$0; nl1=FNR; }     # file1, index by lineno
(NR!=FNR) { ss2[$0]++; }                # file2, index by string
END {
    for (ll=1; ll<=nl1; ll++) if (!(ll1[ll] in ss2)) print ll1[ll]
}

Zapisuje całą zawartość pliku 1 linia po linii w tablicy indeksowanej numerem linii ll1[], a cały zawartość pliku2 linia po linii w tablicy asocjacyjnej indeksowanej liniowo ss2[]. Po wczytaniu obu plików wykonaj iterację ll1 i użyj operatora in, aby określić, czy linia w pliku1 jest obecna w pliku2. (Będzie to miało inne wyjście niż metoda diff, jeśli istnieją duplikaty.)

W przypadku, gdy pliki są wystarczająco duże, że ich przechowywanie powoduje problem z pamięcią, możesz wymienić procesor na pamięć, przechowując tylko plik1 i usuwając dopasowania po drodze jako plik 2 jest odczytywany.

BEGIN { FS="" }
(NR==FNR) {  # file1, index by lineno and string
  ll1[FNR]=$0; ss1[$0]=FNR; nl1=FNR;
}
(NR!=FNR) {  # file2
  if ($0 in ss1) { delete ll1[ss1[$0]]; delete ss1[$0]; }
}
END {
  for (ll=1; ll<=nl1; ll++) if (ll in ll1) print ll1[ll]
}

Powyższy zapisuje całą zawartość pliku 1 w dwóch tablicach, jedną indeksowaną przez numer linii ll1[], jedną indeksowaną przez zawartość linii ss1[]. Następnie podczas odczytu pliku 2, Każda pasująca linia jest usuwana z ll1[] i ss1[]. Na końcu wypisywane są pozostałe linie z pliku1, zachowując pierwotną kolejność.

W tym przypadku, z zaznaczonym problemem, można również podzielić i podbić używając GNU split (filtrowanie jest rozszerzeniem GNU), powtarzane działania z fragmenty pliku1 i odczyt pliku2 za każdym razem:

split -l 20000 --filter='gawk -f linesnotin.awk - file2' < file1

Zwróć uwagę na użycie i umieszczenie - znaczenia stdin w linii poleceń gawk. Jest to dostarczane przez split z pliku 1 w kawałkach 20000 linii na wywołanie.

Dla użytkowników systemów innych niż GNU, prawie na pewno istnieje pakiet GNU coreutils, który można uzyskać, w tym na OSX jako część narzędzi Apple Xcode , które zapewniają GNU diff, awk, jednak tylko POSIX / BSD split zamiast GNU wersja.

 136
Author: mr.spuratic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-13 12:36:24

Komenda comm (skrót od "common") może być przydatna comm - compare two sorted files line by line

#find lines only in file1
comm -23 file1 file2 

#find lines only in file2
comm -13 file1 file2 

#find lines common to both files
comm -12 file1 file2 

Plik man jest do tego całkiem czytelny.

 135
Author: JnBrymn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-18 03:35:59

Jak zasugerował konsolebox, rozwiązanie grep

grep -v -f file2 file1

Faktycznie działa świetnie (szybko), jeśli po prostu dodasz opcję -F, aby traktować wzorce jako stałe ciągi znaków zamiast wyrażeń regularnych. Zweryfikowałem to na parze ~ 1000 liniowych list plików, które musiałem porównać. Z -F zajęło to 0,031 s (real), podczas gdy bez niego zajęło 2,278 s (real), podczas przekierowywania wyjścia grepa do wc -l.

Testy te obejmowały również przełącznik -x, który jest niezbędną częścią roztworu w celu aby zapewnić całkowitą dokładność w przypadkach, gdy plik 2 zawiera linie, które pasują do części, ale nie wszystkich, jednej lub więcej linii w pliku 1.

Więc rozwiązaniem, które nie wymaga sortowania danych wejściowych, jest szybkie ,elastyczne (wielkość liter itp.), a także (myślę) działa na każdym systemie POSIX jest:

grep -F -x -v -f file2 file1
 18
Author: pbz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-07 14:39:33

Jaka jest prędkość as sort i diff?

sort file1 -u > file1.sorted
sort file2 -u > file2.sorted
diff file1.sorted file2.sorted
 10
Author: Puggan Se,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-13 09:12:44
$ join -v 1 -t '' file1 file2
line2
line3

-t upewnia się, że porównuje całą linię, jeśli masz spację w niektórych liniach.

 5
Author: Steven Penny,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-31 23:53:39

Użycie fgrep lub dodanie opcji-F do grep może pomóc. Ale dla szybszych obliczeń można użyć Awk.

Możesz spróbować jednej z tych metod Awk:

Http://www.linuxquestions.org/questions/programming-9/grep-for-huge-files-826030/#post4066219

 1
Author: konsolebox,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-13 09:17:44

Możesz użyć Pythona:

python -c '
lines_to_remove = set()
with open("file2", "r") as f:
    for line in f.readlines():
        lines_to_remove.add(line.strip())

with open("f1", "r") as f:
    for line in f.readlines():
        if line.strip() not in lines_to_remove:
            print(line.strip())
'
 1
Author: HelloGoodbye,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-10 07:24:44

Sposób, w jaki zwykle to robię, to używanie flagi --suppress-common-lines, chociaż zauważ, że działa to tylko wtedy, gdy robisz to w formacie side-by-side.

diff -y --suppress-common-lines file1.txt file2.txt

 1
Author: BAustin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-13 16:22:19

Odkryłem, że dla mnie użycie zwykłej instrukcji pętli if I for działa idealnie.

for i in $(cat file2);do if [ $(grep -i $i file1) ];then echo "$i found" >>Matching_lines.txt;else echo "$i missing" >>missing_lines.txt ;fi;done
 -1
Author: Tman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-01 16:26:28