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


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/^>\ //'

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


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

Входные файлы должны быть отсортированы, чтобы это работало. С помощью bash(и zsh) вы можете отсортировать на месте с заменой процесса <( ):

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.


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


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 , чтобы использовать это решение.


Да, это работает, но даже с -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

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

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


2

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

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

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


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

1

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

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


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

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