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

241

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

Например, если это файл1:

line1
line2
line3

И это файл2:

line1
line4
line5

Тогда мой результат / вывод должен быть:

line2
line3

Это работает:

grep -v -f file2 file1

Но это очень, очень медленно, когда используется на моих больших файлах.

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

Может кто-нибудь помочь мне найти быстрый способ сделать это, используя bash и базовые бинарные файлы Linux?

РЕДАКТИРОВАТЬ: Чтобы продолжить мой вопрос, это лучший способ, который я нашел до сих пор с использованием diff ():

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

Конечно, должен быть лучший способ?

Niels2000
источник
1
Вы можете попробовать это, если это будет быстрее:awk 'NR==FNR{a[$0];next}!($0 in a)' file2 file1 > out.txt
Кент
без быстрого требования: stackoverflow.com/questions/4366533/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
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) скрипт (навеянный скриптами, связанными в ответе Konsolebox), который принимает произвольно упорядоченные входные файлы и выводит пропущенные строки в порядке их появления в file1.

# 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оператор, чтобы определить, присутствует ли строка в файле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]
}

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

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

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.

mr.spuratic
источник
1
Это делает именно то, что мне нужно, за крошечную долю времени, затраченного огромной командой. Спасибо!
Niels2000
1
Нашел эту man-
страницу
некоторые из нас не на GNU [OS X BSD здесь ...] :)
rogerdpack
1
Я предполагаю, что вы имеете в виду diff: в общем случае входные файлы будут другими, diffв этом случае возвращается 1 . Считайте это бонусом ;-) Если вы тестируете в сценарии оболочки, 0 и 1 - ожидаемые коды выхода, 2 указывает на проблему.
mr.spuratic
1
@ mr.spuratic ах да, теперь я нахожу это в man diff. Спасибо!
Archeosudoerus
246

Команда 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Файл на самом деле вполне читаемый для этого.

JnBrymn
источник
6
Работает безупречно на OSX.
Писарук
41
Требование для отсортированного ввода, возможно, следует выделить.
тройной
21
commтакже имеет возможность проверить, что входные данные отсортированы --check-order(что, по-видимому, в любом случае, но эта опция вызовет ошибку вместо продолжения). Но чтобы отсортировать файлы, просто сделайте: com -23 <(sort file1) <(sort file2)и так далее
Майкл
Я сравнивал файл, сгенерированный в Windows, с файлом, сгенерированным в Linux, и казалось, что commон вообще не работает. Мне потребовалось некоторое время, чтобы понять, что речь идет об окончаниях строк: даже строки, которые выглядят одинаково, считаются разными, если имеют разные окончания строк. Команда dos2unixможет использоваться для преобразования концов строк CRLF только в LF.
ZeroOne
23

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

grep -v -f file2 file1

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

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

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

grep -F -x -v -f file2 file1

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

PBZ
источник
Да, это работает, но даже с -Fэтим плохо масштабируется.
Моломби
это не так быстро, я ждал 5 минут 2 файла по ~ 500 тыс. строк, прежде чем сдаться
cahen
на самом деле, этот способ все еще медленнее, чем способ связи, потому что он может обрабатывать несортированные файлы, следовательно, он теряется при сортировке, связь использует преимущество сортировки
workplaylifecycle
@workplaylifecycle Вам нужно добавить время для сортировки, которое может быть узким местом для очень больших file2.
rwst
Тем не менее, grep с -xопцией, очевидно, использует больше памяти. С помощью file2содержащего 180M слов 6-10 байт моего процесса получил Killedв ОЗУ машине 32GB ...
rwst
11

какова скорость сортировки и сравнения?

sort file1 -u > file1.sorted
sort file2 -u > file2.sorted
diff file1.sorted file2.sorted
Puggan Se
источник
1
Спасибо за напоминание о необходимости сортировки файлов перед выполнением diff. sort + diff НАМНОГО быстрее.
Niels2000
4
один лайнер ;-) diff <(сортировка file1 -u) <(сортировка file2 -u)
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требует , чтобы обе входные строки были отсортированы по полю, на котором вы выполняете операцию соединения.
tripleee
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

ИЛИ дайте мне строки в файле1 минус строки в файле2

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

Итак, по сути вы говорите

Найти отдельные строки в файле1 и файле2, а затем дать мне строки в файле1 минус строки в файле2

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

GypsyCosmonaut
источник
2

Может помочь использование fgrep или добавление опции -F в grep. Но для более быстрых вычислений вы можете использовать Awk.

Вы можете попробовать один из этих методов Awk:

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

konsolebox
источник
2
+1 Это единственный ответ, который не требует сортировки входных данных. Хотя ФП, очевидно, был удовлетворен этим требованием, это является неприемлемым ограничением во многих реальных сценариях.
tripleee
1

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

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

BAustin
источник
0

Я обнаружил, что для меня использование нормального оператора цикла 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
Tman
источник
2
Смотрите DontReadLinesWithFor . Кроме того, этот код будет вести себя очень плохо, если какой-либо из ваших grepрезультатов расширится до нескольких слов, или если какие-либо из ваших file2записей могут рассматриваться оболочкой как глобус.
Чарльз Даффи