Швидкий спосіб пошуку рядків в одному файлі, які не є в іншому?


241

У мене є два великих файли (набори імен файлів). Приблизно 30 000 рядків у кожному файлі. Я намагаюся знайти швидкий спосіб пошуку рядків у file1, які відсутні у file2.

Наприклад, якщо це файл1:

line1
line2
line3

І це файл2:

line1
line4
line5

Тоді мій результат / результат повинен бути:

line2
line3

Це працює:

grep -v -f file2 file1

Але це дуже, дуже повільно, коли використовується на моїх великих файлах.

Я підозрюю, що є хороший спосіб зробити це за допомогою diff (), але вихід повинен бути лише рядками, нічого іншого, і я не можу знайти комутатор для цього.

Чи може хто-небудь допомогти мені знайти швидкий спосіб зробити це, використовуючи bash та основні файли бінарних файлів Linux?

EDIT: Щоб продовжити моє власне питання, це найкращий спосіб, який я до цього часу знайшов, використовуючи diff ():

diff file2 file1 | grep '^>' | sed 's/^>\ //'

Звичайно, має бути кращий шлях?


1
Ви можете спробувати це, якщо це швидше:awk 'NR==FNR{a[$0];next}!($0 in a)' file2 file1 > out.txt
Кент,


4
Дякуємо за розповідь про grep -v -f file2 file1
Рахул Прасад


Простий спосіб зі зменшеним набором інструментів: cat file1 file2 file2 | sort | uniq --uniqueдив. Мою відповідь нижче.
Ондра Жижка

Відповіді:


233

Ви можете домогтися цього, керуючи форматуванням старих / нових / незмінних рядків у diffвисновку GNU :

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

Вхідні файли повинні бути відсортовані, щоб це працювало. За допомогою bashzsh) ви можете сортувати на місці із заміною процесу <( ):

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

У вищезазначеному придушуються нові та незмінні рядки, тому виводяться лише змінені (тобто видалені лінії у вашому випадку). Ви також можете використовувати кілька diffваріантів , які інші рішення не пропонують, наприклад , як -iігнорувати випадок, або різні варіанти пробільні ( -E, -b, і -vт.д.) для менш суворого відповідності.


Пояснення

Параметри --new-line-format, --old-line-formatі --unchanged-line-formatдозволяють контролювати шлях diffформатує відмінності, аналогічні printfспецифікатор формату. Ці параметри форматують відповідно нові (додані), старі (видалені) та незмінні рядки. Установлення порожнього "" запобігає виведенню такого рядка.

Якщо ви знайомі з уніфікованим форматом diff , ви можете частково відтворити його за допомогою:

diff --old-line-format="-%L" --unchanged-line-format=" %L" \
     --new-line-format="+%L" file1 file2

Специфікатор %L- це відповідний рядок, і ми префіксуємо кожне з "+" "-" або "", наприклад diff -u (зауважте, що він видає лише відмінності, йому не вистачає рядків --- +++і @@у верхній частині кожної групової зміни). Ви також можете використовувати це , щоб робити інші корисні речі , як числа кожного рядка з %dn.


diffМетод (поряд з іншими пропозиціями commі join) проводити тільки очікуваний результат з відсортованих введенням, хоча ви можете використовувати <(sort ...)для сортування на місці. Ось простий awk(nawk) скрипт (натхненний сценаріями, пов’язаними з відповіддю у відповіді Консолебокса), який приймає довільно впорядковані файли вводу та виводить пропущені рядки у порядку, який вони мають у файлі1.

# 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]
}

Це зберігає весь вміст file1 рядок за рядком в масиві ll1[], індексованому рядком-номером , а весь вміст file2 рядок за рядком в асоційованому масиві, індексованому вмістом рядка ss2[]. Після зчитування обох файлів повторіть ll1і переконайтесь за допомогою inоператора, щоб визначити, чи є рядок у file1 у file2. ( diffЯкщо у нього є дублікати, це буде мати різний вихід до методу.)

Якщо файли є достатньо великими, що їх зберігання викликає проблеми з пам'яттю, ви можете торгувати процесором для пам'яті, зберігаючи лише файл1 і видаляючи відповідність по ходу зчитування файлу2.

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]
}

Вищезазначене зберігає весь вміст file1 у двох масивах, один індексований номером рядка ll1[], один індексований вмістом рядка ss1[]. Потім, коли file2 зчитується, кожен рядок, що відповідає, видаляється з ll1[]та ss1[]. В кінці виводяться решта рядків з файлу1, зберігаючи початковий порядок.

У цьому випадку, із вказаною проблемою, ви також можете розділити та перемогти за допомогою GNU split(фільтрація - це розширення GNU), повторювані запуски з кусками file1 та зчитування файлу2 повністю кожного разу:

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

Зверніть увагу на використання та розміщення -значень stdinу gawkкомандному рядку. Це забезпечується splitз file1 шматками 20000 рядків за викликом.

Для користувачів в системах , НЕ GNU, є майже напевно GNU Coreutils пакет можна отримати, в тому числі на OSX в рамках компанії Apple Xcode інструментів , який забезпечує GNU diff, awkхоча тільки POSIX / BSD , splitа не версія GNU.


1
Це робить саме те, що мені потрібно, за невелику частину часу, зайнятого величезною грепою. Дякую!
Niels2000

1
Знайшли цю сторінку gnu
1313

дехто з нас не на Gnu [OS X bsd тут ...] :)
rogerdpack

1
Я припускаю, що ви маєте на увазі diff: загалом вхідні файли будуть різними, diffу цьому випадку повертається 1 . Вважайте це бонусом ;-) Якщо ви протестуєте в скрипті оболонки 0 і 1 очікуються коди виходу, 2 вказує на проблему.
mr.spuratic

1
@ mr.spuratic ах так, тепер я знаходжу це в man diff. Дякую!
Archeosudoerus

246

Команда comm (скорочення "загальний") може бути корисною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 

manФайл насправді цілком читається для цього.


6
Працює бездоганно на OSX.
писарук

41
Можливо, слід підкреслити вимогу до сортованого введення.
трійчатка

21
commтакож є можливість перевірити впорядкований вхід --check-order(що, здається, все одно, але ця опція призведе до помилки замість продовження). Але для сортування файлів просто зробіть: com -23 <(sort file1) <(sort file2)і так далі
michael

Я порівнював файл, який був створений в Windows, з файлом, створеним в Linux, і здавалося, що commвін взагалі не працює. Мені знадобилося певний час, щоб зрозуміти, що мова йде про закінчення рядків: навіть рядки, які виглядають однаково, вважаються різними, якщо вони мають різні закінчення рядків. Команда dos2unixможе використовуватися для перетворення закінчень рядків CRLF лише в LF.
ZeroOne

23

Як і запропоновано konsolebox, плакати-греп-рішення

grep -v -f file2 file1

насправді чудово працює (швидко), якщо просто додати -Fпараметр, трактувати візерунки як фіксовані рядки замість регулярних виразів. Я перевірив це на пару ~ 1000 списків файлів рядків, які мені довелося порівняти. З -Fним знадобилося 0,031 с (реально), тоді як без цього знадобилося 2,227 с (реально), коли перенаправлення греп виведення на wc -l.

Ці тести також включали -xкомутатор, який є необхідною частиною рішення для того, щоб забезпечити повну точність у випадках, коли file2 містить рядки, які відповідають частині одного, але не всіх, одного або декількох рядків у файлі1.

Таким чином, рішення, яке не вимагає сортування входів, є швидким, гнучким (чутливість регістру тощо):

grep -F -x -v -f file2 file1

Це не працює з усіма версіями grep, наприклад, він не працює в macOS, де рядок у файлі 1 буде показаний як такий, який не присутній у файлі 2, навіть якщо він відповідає іншому рядку, який є його підрядком . Крім того, ви можете встановити GNU grep на macOS для використання цього рішення.


Так, це працює, але навіть -Fце не так добре.
Моломбі

це не так швидко, я зачекав 5 хвилин на 2 файли ~ 500k рядків, перш ніж здаватися
cahen

насправді цей спосіб все ще повільніше, ніж комунікаційний, тому що цей може обробляти несортовані файли, отже, перетягування шляхом несортування, comm користується перевагою сортування
workplaylifecycle

@workplaylifecycle Вам потрібно додати час для сортування, яке може бути вузьким місцем для надзвичайно великих file2.
rwst

Однак, grep з -xопцією, мабуть, використовує більше пам'яті. З file2вмістом 180M слів 6-10 байт мій процес потрапив Killedна 32 Гб оперативної пам'яті ...
rwst

11

яка швидкість як сортування, так і різниці?

sort file1 -u > file1.sorted
sort file2 -u > file2.sorted
diff file1.sorted file2.sorted

1
Дякуємо, що нагадали мені про необхідність сортування файлів, перш ніж робити розл. Сортувати + різниться МНОГО швидше.
Niels2000

4
один лайнер ;-) diff <(сортувати файл1 -у) <(сортувати файл2 -у)
steveinatorx

11

Якщо вам не вистачає "вигадливих інструментів", наприклад, у мінімальному дистрибутиві Linux, є рішення з просто cat, sortі uniq:

cat includes.txt excludes.txt excludes.txt | sort | uniq --unique

Тест:

seq 1 1 7 | sort --random-sort > includes.txt
seq 3 1 9 | sort --random-sort > excludes.txt
cat includes.txt excludes.txt excludes.txt | sort | uniq --unique

# Output:
1
2    

Це також порівняно швидко grep.


1
Примітка - деякі реалізації не визнають --uniqueваріант. Ви повинні мати можливість використовувати для цього стандартизовану опцію POSIX :| uniq -u
AndrewF

1
У прикладі, звідки взялося "2"?
Niels2000

1
@ Niels2000, seq 1 1 7створює числа від 1, з кроком 1, до 7, тобто 1 2 3 4 5 6 7. А тут є свої 2!
Ейрік Лігре

5
$ join -v 1 -t '' file1 file2
line2
line3

-tПереконується , що він порівнює всю рядок, якщо ви мали місце в деяких з ліній.


Можливо comm, joinвимагає , щоб обидва вхідні рядки були відсортовані в полі, на якому виконується операція з'єднання.
трійчатка

4

Ви можете використовувати Python:

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())
'

4

Використовуйте combineз moreutilsпакета, утиліта наборів , яка підтримує not, and, or, xorоперації

combine file1 not file2

тобто дайте мені рядки, які знаходяться у file1, але не у file2

АБО дайте мені рядки у file1 мінус рядки у file2

Примітка: combine сортує та знаходить унікальні рядки в обох файлах перед виконанням будь-якої операції, але diffне виконує. Таким чином, ви можете знайти відмінності між виходом diffта combine.

Так ви фактично говорите

Знайдіть окремі рядки у file1 та file2, а потім дайте мені рядки у file1 мінус рядки у file2

На мій досвід, це набагато швидше, ніж інші варіанти


2

Використання fgrep або додавання опції -F до grep може допомогти. Але для більш швидких обчислень ви можете використовувати Awk.

Ви можете спробувати один із таких методів Awk:

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


2
+1 Це єдина відповідь, яка не вимагає сортування вхідних даних. Незважаючи на те, що ОП була задоволена цією вимогою, це є неприйнятним обмеженням у багатьох реальних сценаріях.
трійчатка

1

Як я зазвичай це роблю, використовую --suppress-common-linesпрапор, хоча зауважте, що це працює лише в тому випадку, якщо ви робите це у форматному порядку.

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


0

Я виявив, що для мене використання нормального, якщо і для циклу операція працювала ідеально.

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

2
Див. DontReadLinesWithFor . Крім того, цей код буде вести себе дуже погано, якщо будь-який з ваших grepрезультатів розшириться на кілька слів або якщо будь-яка з ваших file2записів може розглядатися оболонкою як глобус.
Чарльз Даффі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.