Быстрый способ нахождения строк в одном файле, которые не находятся в другом?

У меня есть два больших файла (наборы имен файлов). Примерно 30.000 строк в каждом файле. Я пытаюсь найти быстрый способ нахождения строк в файле1, которых нет в файле2.

Например, если это файл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

Вы можете добиться этого, контролируя форматирование старых/новых/неизменных строк в выводе GNU diff:

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) script (вдохновленный сценариями, связанными с Konsolebox), который принимает произвольно упорядоченные входные файлы и выводит недостающие строки в том порядке, в котором они происходят в файле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]
}

Это сохраняет все содержимое файла1 по строкам в индексированном массиве с номером строки ll1[], а все содержимое файла2 строит строку в индексированном ассоциативном массиве ss2[]. После чтения обоих файлов выполните итерацию по ll1 и используйте оператор in, чтобы определить, присутствует ли строка в файле1 в файле2. (При наличии дубликатов это будет иметь другой выход для метода 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]
}

Вышеприведенное содержимое хранит все содержимое файла1 в двух массивах, индексируется номером строки ll1[], индексируется по контенту ss1[]. Затем, когда файл2 считывается, каждая соответствующая строка удаляется из ll1[] и ss1[]. В конце выводятся оставшиеся строки из файла1, сохраняя исходный порядок.

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

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

Обратите внимание на использование и размещение -, что означает stdin в командной строке gawk. Это обеспечивается split из файла1 в кусках 20000 строк за вызов.

Для пользователей в системах, отличных от GNU, почти наверняка есть пакет GNU coreutils, который можно получить, в том числе на OSX как часть Apple Xcode инструменты, которые обеспечивают GNU diff, awk, хотя и только POSIX/BSD split, а не версию GNU.

Ответ 2

Команда comm (сокращение от "common") может быть полезной 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 на самом деле вполне читабелен для этого.

Ответ 3

Как и в случае с konsolebox, решение grep для постеров

grep -v -f file2 file1

на самом деле работает отлично (быстро), если вы просто добавите опцию -F, чтобы обрабатывать шаблоны как фиксированные строки вместо регулярных выражений. Я проверил это на паре списков ~ 1000 строк, которые мне пришлось сравнить. С -F это заняло 0,031 с (реальное), в то время как без него - 2,278 с (реальное) при перенаправлении вывода grep на wc -l.

Эти тесты также включали переключатель -x, который является необходимой частью решения для обеспечения полной точности в случаях, когда file2 содержит строки, которые соответствуют части, но не всем, одной или более строк в file1.

Таким образом, решение, которое не требует сортировки входных данных, является быстрым, гибким (чувствительность к регистру и т.д.):

grep -F -x -v -f file2 file1

Это не работает со всеми версиями grep, например, происходит сбой в macOS, где строка в файле 1 будет отображаться как отсутствующая в файле 2, даже если она соответствует другой строке, которая является подстрокой этого Кроме того, вы можете установить GNU grep на macOS, чтобы использовать это решение.

Ответ 4

Какова скорость сортировки и diff?

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

Ответ 5

Если вам не хватает "модных инструментов", например, в некоторых минимальных дистрибутивах 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.

Ответ 6

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

-t гарантирует, что он сравнивает всю строку, если у вас есть пробел в некоторых строках.

Ответ 8

Вы можете использовать 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())
'

Ответ 9

Обычно я делаю это, используя флаг --suppress-common-lines, хотя учтите, что это работает, только если вы делаете это в формате рядом друг с другом.

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

Ответ 10

Используйте combine из пакета moreutils, утилиту множеств, которая поддерживает операции not, and, or, xor

combine file1 not file2

то есть, дайте мне строки, которые есть в file1, но не в file2

По моему опыту, это намного быстрее, чем другие варианты

Ответ 11

Я обнаружил, что для меня использование нормального оператора if и for отлично работает.

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