Как удалить дубликаты строк внутри текстового файла?

126

Огромный (до 2 ГиБ) мой текстовый файл содержит около 100 точных дубликатов каждой строки в нем (в моем случае это бесполезно, поскольку файл представляет собой таблицу данных, похожую на CSV).

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

Я написал программу на Scala (рассмотрим Java, если вы не знаете о Scala), чтобы реализовать это. Но, может быть, есть более быстрые собственные инструменты, написанные на C, способные сделать это быстрее?

ОБНОВЛЕНИЕ: awk '!seen[$0]++' filenameрешение, казалось, работало очень хорошо для меня, пока файлы были около 2 ГБ или меньше, но теперь, когда я собираюсь очистить файл 8 ГБ, оно больше не работает. Кажется, что бесконечность на Mac с 4 ГБ ОЗУ и 64-битном ПК с Windows 7 с 4 ГБ ОЗУ и подкачкой 6 ГБ просто не хватает памяти. И я не испытываю энтузиазма по поводу того, чтобы попробовать это на Linux с 4 ГБ RAM, учитывая этот опыт.

Иван
источник
это разрушит ваш порядок, но, если вы пробовали сортировать -u, я понятия не имею, как или если он может работать на таком массивном файле
0x7c0
5
C часто не значительно быстрее, чем Java, и если вы запускаете его (по порядку) сейчас, есть большая вероятность, что он завершится до того, как вы получите ответ здесь, внедрите его, и он завершит работу; не в порядке, sort -uвероятно, будет быстрее.
Кевин

Ответы:

215

awkРешение видно на #bash (Freenode):

awk '!seen[$0]++' filename
enzotib
источник
1
Просто попробовал это на 2G файле, и это заняло три минуты на моем ноутбуке. Неплохо. Я также попытался uniq имя файла | awk '! seen [$ 0] ++', но это было не так быстро.
mgjk
Это на удивление быстрее, чем более подробная awkверсия, использующая 2 поиска в массивах (показано в расширенном объяснении в ответе Жиля): 0m36.132s против 0m49.958s .. для 50 миллионов строк ... Я думал, что узким местом будет ввод / вывод, но дополнительный поиск в массиве ... 1 миллион элементов в массиве, кажется, делает довольно существенную вмятину ...
Peter.O
Но как это сравнить с сортировкой -u ....?
HashWizard
1
@HashWizard: эта команда не сортирует, но устраняет все последующие вхождения одной и той же строки
enzotib
1
@MaxWilliams да, это работает, они случайным образом распределены.
Setholopolus
47

Существует простой (не сказать очевидный) метод, использующий стандартные утилиты, который не требует большого объема памяти, кроме как для запуска sort, который в большинстве реализаций имеет специфические оптимизации для больших файлов (хороший алгоритм внешней сортировки). Преимущество этого метода в том, что он зацикливается только на всех строках внутри специальных утилит, а не внутри интерпретируемых языков.

<input nl -b a -s : |           # number the lines
sort -t : -k 2 -u |             # sort and uniquify ignoring the line numbers
sort -t : -k 1n |               # sort according to the line numbers
cut -d : -f 2- >output          # remove the line numbers

Если все строки начинаются с непробельного символа, вы можете обойтись без некоторых параметров:

<input nl | sort -k 2 -u | sort -k 1n | cut -f 2- >output

Для большого количества дублирования метод, который требует только сохранения одной копии каждой строки в памяти, будет работать лучше. С некоторыми дополнительными интерпретациями для этого есть очень лаконичный сценарий awk (уже опубликованный enzotib ):

<input awk '!seen[$0]++'

Менее сжато: !seen[$0] {print} {seen[$0] += 1}то есть вывести текущую строку, если она еще не видна, затем увеличить seenсчетчик для этой строки (неинициализированные переменные или элементы массива имеют числовое значение 0).

Для длинных строк вы можете сэкономить память, сохраняя только несанкционированную контрольную сумму (например, криптографический дайджест) каждой строки. Например, используя SHA-1, вам нужно всего 20 байтов плюс постоянные издержки на строку. Но вычисление дайджестов происходит довольно медленно; Этот метод выиграет, только если у вас быстрый ЦП (особенно с аппаратным ускорителем для вычисления дайджестов) и не достаточно памяти относительно размера файла и достаточно длинных строк. Никакая базовая утилита не позволяет вычислить контрольную сумму для каждой строки; вам придется нести ответственность за интерпретацию Perl / Python / Ruby /… или написать специальную скомпилированную программу.

<input perl -MDigest::MD5 -ne '$seen{Digest::MD5::md5($_)}++ or print' >output
жилль
источник
@ Жиль На основании вашего объяснения awk '!seen[$0]++', означает ли это, что если awk видит 2 повторяющиеся строки, он всегда будет сохранять первую и игнорировать все последующие? (Или он сохранит последний?)
user779159
1
@ user779159 Он сохраняет первый: каждая строка ввода печатается сразу (первое вхождение) или не печатается вообще (повторное вхождение).
Жиль
Но как это сравнить с сортировкой -u ...?
HashWizard
@HashWizard Обычная sort -uменяет порядок. Мой ответ показывает решения, которые сохраняют порядок (точнее, порядок первых вхождений).
Жиль
@ Жиль, вы бы сказали, что это быстрее, чем sort -u для больших файлов (10G) с 50% дубликатов?
HashWizard
25
sort -u big-csv-file.csv > duplicates-removed.csv

Обратите внимание, что выходной файл будет отсортирован.

Владислав Довгальец
источник
1
Не так быстро, как awkкоманда в других ответах, но концептуально просто!
Иоганн
@Johann Я делаю это довольно часто для файлов с сотнями тысяч (даже миллионами) коротких строк, заканчивающихся символом новой строки. Я получаю результаты довольно быстро для экспериментов, которые я делаю. Это может быть более важно, если использовать в сценариях, которые запускаются снова и снова, экономия времени может быть значительной.
Владислав Довгальец
1
Используйте sort -uдля удаления дубликатов во время сортировки, а не после. (И экономит пропускную способность памяти), передавая его в другую программу). Это лучше, чем awkверсия, если вы тоже хотите отсортировать вывод. (ОП по этому вопросу хочет сохранить свое первоначальное упорядочение , так что это хороший ответ для немного другого варианта использования.)
Питер Кордес
Мне потребовалось около минуты для файла с 5,5 миллионами строк (всего 1,8 ГБ). Brilliant.
Макс Уильямс
18

Предполагая, что вы можете позволить себе хранить в памяти столько же дедуплицированного файла (если ваши данные действительно дублируются с коэффициентом 100, что должно составлять около 20 МБ + накладные расходы), вы можете сделать это очень легко с помощью Perl.

$ perl -ne 'print unless $dup{$_}++;' input_file > output_file

Это сохраняет порядок тоже.

Вы можете извлечь количество вхождений каждой строки из %dupхэша, если хотите, в качестве дополнительного бесплатного бонуса.

Если вы предпочитаете awk, это должно быть сделано тоже самое (та же логика, что и в версии perl, тот же порядок, те же данные, собранные в dupпеременной):

$ awk '{if (++dup[$0] == 1) print $0;}' input_file > output_file
Мат
источник
Это слишком хорошо @Mat, я собирался вырвать файл, смеется ;-).
Nikhil Mulley
Теперь ждет @ManAtWork его SED и AWK волшебную weavery тоже :-)
Никхилу Mulley
снова здорово за совет по awk :-)
Nikhil Mulley
1
Можно ли изменить скрипт perl, чтобы удалить только дубликаты смежных строк?
Дамблдад
2
@ Dumbledad: uniqделает это все сам по себе
Мат
3

Поскольку никакой другой ответ не предоставил поддержку на месте, вот один:

gawk -i inplace '!a[$0]++' file
Ян Chren - Rindeal
источник
Сохраняет ли это порядок? Кстати, это не сработало для меня. Моя версия:GNU Awk 4.0.2
Леонид
1
@ Леонид да, это так. Он печатает первое вхождение любой уникальной строки. Поддержка на месте была впервые представлена ​​в версии 4.1, которая была выпущена в 2013 году.
Ян Крен - rindeal
3

Вы можете использовать uniq http://www.computerhope.com/unix/uuniq.htm

uniq сообщает или отфильтровывает повторяющиеся строки в файле.

Махмуд Залт
источник
Когда вы даете ответ, предпочтительно дать какое-то объяснение, ПОЧЕМУ ваш ответ тот. Итак, чем этот ответ отличается от нескольких предыдущих ответов?
Стивен Раух
1
С man-страницы uniq: Примечание: 'uniq' does not detect repeated lines unless they are adjacent. так что вам нужно сначала отсортировать его и потерять порядок не повторяющихся строк.
Виндолин
2

Лайнеры Python One:

python -c "import sys; lines = sys.stdin.readlines(); print ''.join(sorted(set(lines)))" < InputFile
Рахул Патил
источник
это приводит к тому, что весь файл заносится в память и может не подходить для проблемы ОП. Также не гарантировано сохранение заказа
iruvar
Спасибо за предложение, я только что изучил Python .. Просто попробовал это для учебной цели .. :)
Рахул Патил
Вот версия Python 2.7, которая не является однострочной, но (лаконично) возвращает уникальный порядок сохранения строк без загрузки всего файла в память или создания одной гигантской строки для подачи на печать
iruvar
Спасибо @ 1_CR У меня есть кое-что узнать сегодня :)OrderedDict
Рахул Патил
0

Ни один из ответов здесь не работал для меня на моем Mac, поэтому я написал простой скрипт на python, который работает для меня. Я игнорирую начальные / конечные пробелы, а также не заботится о потреблении памяти.

import sys

inputfile = sys.argv[1]
outputfile = sys.argv[2]

with open(inputfile) as f:
    content = f.readlines()

content = [x.strip() for x in content]

my_list = list(set(content))

with open(outputfile, 'w') as output:
    for item in my_list:
        output.write("%s\n" % item)

Сохраните вышеупомянутое в unique.py и запустите так:

python unique.py inputfile.txt outputfile.txt
Джаред
источник
-1

С bash 4 можно использовать решение с чистым bash, использующее преимущества ассоциативных массивов . Вот пример

unset llist; declare -A llist;
while read -r line; do
if [[ ${llist[$line]} ]]; then
  continue
else 
  printf '%s\n' "$line"
  llist[$line]="x"
fi
done < file.txt
Iruvar
источник
2
Не используйте readциклы для обработки больших текстовых файлов. bash должен читать по одному байту за раз, чтобы избежать перекоса новой строки. Bash также не очень быстр в обработке текста по сравнению с awk. Если вы воспользуетесь этим, вы не будете использовать read -raобратную косую черту в своем входе. Кроме того, не забудьте unset llist после цикла, если вы поместите это в функцию оболочки или используете ее в интерактивном режиме.
Питер Кордес
2
@PeterCordes, или вы могли только что сослаться на это :-)
iruvar