удалить дубликаты из многих файлов csv

Учитывая n CSV-файлов, в которых они составляют до 100 ГБ, мне нужно удалить повторяющиеся строки на основе следующих правил и условий:

  • Файлы csv пронумерованы от 1.csv до n.csv, и каждый файл имеет размер около 50 МБ.
  • Первый столбец является строковым ключом, 2 строки считаются дубликатами, если их первые столбцы совпадают.
  • Я хочу удалить дубликаты, сохранив их в более позднем файле (2.csv считается более поздним, чем 1.csv)

Мой алгоритм следующий, я хочу знать, есть ли лучший.

  • объединить все файлы в один гигантский файл

    cat *.csv > one.csv
    
  • сортировать CSV

    sort one.csv >one_sorted.csv
    
  • не уверен, как устранить дубликаты на данный момент. uniq имеет флаг -f, который пропускает первые N полей, но в моем случае я хочу пропустить все поля, кроме первого 1.

Мне нужна помощь с последним шагом (устранение дубликатов в отсортированном файле). Также есть ли более эффективный алгоритм?


person user121196    schedule 15.10.2012    source источник
comment
Если вы не пометите строки номером файла, сортировка будет означать, что вы больше не можете удовлетворять третьему условию (выигрывает более поздняя запись). По крайней мере, sort -o one_sorted.csv *.csv будет занимать меньше места на диске и быстрее, чем вариант cat + sort.   -  person Jonathan Leffler    schedule 15.10.2012
comment
Сколько дубликатов вы ожидаете найти? Как вы думаете, насколько большим будет дедуплицированный вывод?   -  person Jonathan Leffler    schedule 15.10.2012
comment
@ Джонатан Леффлер: если мы не можем соблюсти третье условие, давайте забудем об этом. Дубликатов очень мало (вероятно, менее 3%).   -  person user121196    schedule 15.10.2012
comment
Насколько велика каждая запись (в среднем)? Насколько велика каждая клавиша (в среднем)? Насколько изменчив размер? Насколько велика машина, на которой выполняется эта рабочая нагрузка (какова основная память на машине)?   -  person Jonathan Leffler    schedule 15.10.2012


Ответы (4)


Если вы можете сохранить строки в памяти

Если в памяти помещается достаточно данных, awk решение by steve довольно аккуратно, пишете ли вы команду sort по каналу внутри awk или просто передаете вывод без украшений от awk до sort на уровне оболочки.

Если у вас есть 100 ГБ данных с дублированием, возможно, 3%, вам нужно будет хранить 100 ГБ данных в памяти. Это много основной памяти. 64-битная система может справиться с виртуальной памятью, но она, скорее всего, будет работать довольно медленно.

Если ключи помещаются в памяти

Если вы не можете уместить достаточно данных в памяти, то предстоящая задача намного сложнее и потребует как минимум двух сканирований файлов. Мы должны заранее предположить, что вы можете, по крайней мере, поместить каждый ключ в память, а также подсчитать, сколько раз ключ появлялся.

  1. Scan 1: read the files.
    • Count the number of times each key appears in the input.
    • В awk используйте icount[$1]++.
  2. Scan 2: reread the files.
    • Count the number of times each key has appeared; ocount[$1]++.
    • Если icount[$1] == ocount[$1], то вывести строку.

(Это предполагает, что вы можете сохранить ключи и счетчики дважды; альтернативой является использование icount (только) в обоих сканированиях, увеличение при сканировании 1 и уменьшение при сканировании 2, печать значения, когда счетчик уменьшается до нуля.)

Я бы, вероятно, использовал для этого Perl, а не awk, хотя бы потому, что в Perl будет легче перечитывать файлы, чем в awk.


Даже ключи не подходят?

А что, если вы даже не можете поместить ключи и их количество в память? Тогда вы столкнетесь с некоторыми серьезными проблемами, не в последнюю очередь из-за того, что языки сценариев могут не сообщать вам о состоянии нехватки памяти так четко, как вам хотелось бы. Я не буду пытаться пересечь этот мост, пока не будет доказано, что это необходимо. И если это необходимо, нам понадобятся некоторые статистические данные о наборах файлов, чтобы знать, что может быть возможно:

  • Средняя длина записи.
  • Количество отдельных ключей.
  • Количество различных ключей с N вхождениями для каждого из N = 1, 2, ... max.
  • Длина ключа.
  • Количество ключей плюс счетчики, которые можно поместить в память.

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


Perl-решение

Пример данных

$ cat x000.csv
abc,123,def
abd,124,deg
abe,125,deh
$ cat x001.csv
abc,223,xef
bbd,224,xeg
bbe,225,xeh
$ cat x002.csv
cbc,323,zef
cbd,324,zeg
bbe,325,zeh
$ perl fixdupcsv.pl x???.csv
abd,124,deg
abe,125,deh
abc,223,xef
bbd,224,xeg
cbc,323,zef
cbd,324,zeg
bbe,325,zeh
$ 

Обратите внимание на отсутствие гигабайтного тестирования!

fixdupcsv.pl

При этом используется метод «считай вверх, отсчитывай вниз».

#!/usr/bin/env perl
#
# Eliminate duplicate records from 100 GiB of CSV files based on key in column 1.

use strict;
use warnings;

# Scan 1 - count occurrences of each key

my %count;
my @ARGS = @ARGV;   # Preserve arguments for Scan 2

while (<>)
{
    $_ =~ /^([^,]+)/;
    $count{$1}++;
}

# Scan 2 - reread the files; count down occurrences of each key.
# Print when it reaches 0.

@ARGV = @ARGS;      # Reset arguments for Scan 2

while (<>)
{
    $_ =~ /^([^,]+)/;
    $count{$1}--;
    print if $count{$1} == 0;
}

Нотация 'while (<>)' уничтожает @ARGV (отсюда копирование в @ARGS перед выполнением каких-либо других действий), но это также означает, что если вы сбросите @ARGV до исходного значения, он будет проходить через файлы во второй раз. Протестировано с Perl 5.16.0 и 5.10.0 в Mac OS X 10.7.5.

Это Перл; TMTOWTDI. Вы можете использовать:

#!/usr/bin/env perl
#
# Eliminate duplicate records from 100 GiB of CSV files based on key in column 1.

use strict;
use warnings;

my %count;

sub counter
{
    my($inc) = @_;
    while (<>)
    {
        $_ =~ /^([^,]+)/;
        $count{$1} += $inc;
        print if $count{$1} == 0;
    }
}

my @ARGS = @ARGV;   # Preserve arguments for Scan 2
counter(+1);
@ARGV = @ARGS;      # Reset arguments for Scan 2
counter(-1);

Вероятно, есть способы сжать тело цикла, но я нахожу то, что там достаточно ясно, и предпочитаю ясность предельной краткости.

Вызов

Вам нужно представить сценарий fixdupcsv.pl с именами файлов в правильном порядке. Поскольку у вас есть файлы с номерами от 1.csv до примерно 2000.csv, важно не перечислять их в алфавитно-цифровом порядке. Другие ответы предлагают ls -v *.csv использовать параметр расширения GNU ls. Если он доступен, это лучший выбор.

perl fixdupcsv.pl $(ls -v *.csv)

Если это недоступно, вам нужно выполнить числовую сортировку имен:

perl fixdupcsv.pl $(ls *.csv | sort -t. -k1.1n)

Awk-решение

awk -F, '
BEGIN   {
            for (i = 1; i < ARGC; i++)
            {
                while ((getline < ARGV[i]) > 0)
                    count[$1]++;
                close(ARGV[i]);
            }
            for (i = 1; i < ARGC; i++)
            {
                while ((getline < ARGV[i]) > 0)
                {
                    count[$1]--;
                    if (count[$1] == 0) print;
                }
                close(ARGV[i]);
            }
        }' 

Это игнорирует врожденный цикл чтения awk и делает все чтение явно (вы можете заменить BEGIN на END и получить тот же результат). Логика во многих отношениях тесно связана с логикой Perl. Протестировано на Mac OS X 10.7.5 с BSD awk и GNU awk. Интересно, что GNU awk настаивал на скобках в вызовах close, тогда как BSD awk этого не делал. Вызовы close() необходимы в первом цикле, чтобы второй цикл вообще работал. Вызовы close() во втором цикле нужны для сохранения симметрии и аккуратности, но они также могут быть важны, когда вы обрабатываете несколько сотен файлов за один прогон.

person Jonathan Leffler    schedule 15.10.2012
comment
+1 Да, я согласен, если у ОП нет доступа к достаточно большой машине, ему придется ее «Perl». С awk это будет непросто. Мне нравится второй (альтернативный подход), потому что это самый эффективный способ, особенно если ключи очень большие. - person Steve; 15.10.2012
comment
@Jonathan Leffler: ключи впишутся в память. Не могли бы вы предоставить реальный код в awk? - person user121196; 15.10.2012
comment
Если бы я мог проголосовать несколько раз, я бы это сделал. Мне нравится решение perl, но оно могло бы быть более привлекательным. Например, вы можете заменить первый цикл на $count{ (split(',', $_))[0] }++ while <>;. Это также может быть немного более ремонтопригодным. Не забудьте также указать, как запустить зверя: ./script.pl $(ls -v *.csv). - person Steve; 16.10.2012
comment
@ user121196: Решение perl должно быть принятым ответом. - person Steve; 16.10.2012
comment
@steve: Это Perl — TMTOWTDI! В интересном варианте одна и та же функция используется дважды с аргументами +1 и -1: my @ARGS = @ARGV; counter(+1); @ARGV = @ARGs; counter(-1);, а подпрограмма counter выполняет чтение и печать. Из-за приращения он никогда ничего не напечатает при первом вызове; из-за декремента он будет напечатан при втором вызове. Я не уверен, что разбиение всей строки, а затем отбрасывание всего, кроме первого поля разбиения, эффективно. - person Jonathan Leffler; 16.10.2012
comment
Хорошее улучшение: это то, что мне нравится видеть :-) Последний пункт; У меня всегда было впечатление, что получение части разделенного вызова было быстрее или, по крайней мере, так же быстро, как регулярное выражение. Но, видимо, я ошибаюсь (см. пункт 7 здесь< /а>). Спасибо за совет! - person Steve; 16.10.2012
comment
@Jonathan Lefflier: в версии Perl есть проблема, если какой-либо файл csv не заканчивается новой строкой. - person user121196; 20.01.2013
comment
@user121196 user121196: Это остается в качестве упражнения для читателя. Вы можете жевать линии по мере их поступления. Вы можете работать с определением, что текстовые файлы всегда заканчиваются новой строкой; если это неправильно сформированный файл (не оканчивающийся новой строкой), GIGO — Garbage In, Garbage Out. Это рутинные вещи, с которыми вы должны иметь дело, не моргнув глазом — ну, через двадцать лет вы сможете делать это, не моргнув глазом. - person Jonathan Leffler; 20.01.2013

Вот один из способов использования GNU awk:

awk -F, '{ array[$1]=$0 } END { for (i in array) print array[i] }' $(ls -v *.csv)

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

Вы можете сделать это, если вам требуется числовая сортировка в первом столбце:

awk -F, '{ array[$1]=$0 } END { for (i in array) print array[i] | "sort -nk 1" }' $(ls -v *.csv)
person Steve    schedule 15.10.2012
comment
+1: Это довольно удобно, если awk может хранить одну запись для каждого уникального ключа в памяти. - person Jonathan Leffler; 15.10.2012
comment
С ~ 100 ГБ текстовых данных у меня есть сомнения :} - person tink; 15.10.2012
comment
@steve: программа сломается, если ей не хватит памяти? - person user121196; 15.10.2012
comment
@ user121196: Вся ваша система сломается, если она будет потреблять больше памяти, чем доступно. Доступ к большим машинам значительно облегчает жизнь. - person Steve; 15.10.2012

Мой ответ основан на steve

awk -F, '!count[$1]++' $(ls -rv *.csv)

{print $0} подразумевается в операторе awk.

По сути, awk печатает только первую строку, $1 которой содержит это значение. Поскольку файлы .csv перечислены в обратном естественном порядке, это означает, что для всех строк с одинаковым значением $1 печатается только строка из последнего файла.

Примечание. Это не работает, если у вас есть дубликаты в одном файле (т. е. если у вас есть несколько экземпляров одного и того же ключа в одном файле).

person doubleDown    schedule 15.10.2012
comment
Проблема запрашивает последнюю строку с тем же ключом, а не первую строку с данным ключом. Это усложняет жизнь! И почему механизм не будет работать, если у вас есть дубликаты в одном файле? Похоже, мне должно быть хорошо. - person Jonathan Leffler; 15.10.2012
comment
@JonathanLeffler: Да, изначально я рассматривал именно этот метод, но, к сожалению, массив не будет содержать последние ключи, если они дублируются из одного и того же файла. Он будет содержать первый экземпляр ключа. - person Steve; 15.10.2012
comment
@JonathanLeffler: ОП явно упомянул дублирование (один и тот же ключ) в разных файлах (в этом случае код будет работать). Но он не упомянул дупликации внутри одного файла (в этом случае код не будет работать - это, вероятно, ваша основная проблема здесь, но обратите внимание, что я упомянул это самое ограничение в Примечании в моем ответе) - person doubleDown; 15.10.2012
comment
ОК — теперь я вижу, что я упустил и что вы тщательно не выделили. Это буква r в ваших вариантах до ls. Это список файлов в обратном порядке. Это также объясняет, почему ваш алгоритм не будет работать, если в одном файле есть повторяющиеся записи. Наверное, вам стоило выделить эти моменты. - person Jonathan Leffler; 15.10.2012
comment
@JonathanLeffler: Да, цитирую... Поскольку файлы .csv перечислены в обратном естественном порядке - person doubleDown; 15.10.2012
comment
Я вижу, что упустил ключевой момент вашего объяснения (который был опцией r), хотя вы упомянули обратный порядок обработки файлов. Я пошутил. Прошу прощения. (Мне не нравится ограничение «никаких дубликатов в одном файле»; кажется, что там будут проблемы. Но это было четко выделено.) - person Jonathan Leffler; 15.10.2012

Что касается вашего плана сортировки, может быть более практичным сортировать отдельные файлы, а затем объединять их, а не объединять, а затем сортировать. Сложность сортировки с помощью программы sort, скорее всего, будет O(n log(n)). Если у вас, скажем, 200 000 строк на файл размером 50 МБ и 2000 файлов, n будет около 400 миллионов, а n log(n) ~ 10^10. Если вместо этого вы обрабатываете F файлов R записей по отдельности, стоимость сортировки составляет O(F*R*log(R)), а стоимость слияния составляет O(F*R*log(R)). Эти затраты достаточно высоки, чтобы раздельная сортировка не обязательно была быстрее, но процесс можно разбить на удобные фрагменты, чтобы его было легче проверять по ходу дела. Вот небольшой пример, который предполагает, что запятая может использоваться в качестве разделителя для ключа сортировки. (Ключевое поле, разделенное кавычками и содержащее запятые, будет проблемой для показанной сортировки.) Обратите внимание, что -s указывает sort выполнять стабильную сортировку, оставляя строки с одним и тем же ключом сортировки в том порядке, в котором они встречались.

for i in $(seq 1 8); do sort -t, -sk1,1 $i.csv > $i.tmp; done
sort -mt, -sk1,1 [1-8].tmp > 1-8.tmp

или, если быть более осторожным, можно сохранить некоторые промежуточные результаты:

sort -mt, -sk1,1 [1-4].tmp > 1-4.tmp
sort -mt, -sk1,1 [5-8].tmp > 5-8.tmp
cp 1-4.tmp 5-8.tmp /backup/storage
sort -mt, -sk1,1 1-4.tmp 5-8.tmp > 1-8.tmp

Кроме того, преимущество выполнения отдельных сортировок с последующим слиянием или слиянием заключается в простоте разделения рабочей нагрузки между несколькими процессорами или системами.

После того, как вы отсортируете и объедините все файлы (скажем, в файл X), довольно просто написать awk-программу, которая в BEGIN читает строку из X и помещает ее в переменную L. После этого каждый раз, когда она читает строку из X , если первое поле $0 не совпадает с L, оно записывает L и устанавливает L равным $0. Но если $0 соответствует L, L устанавливается в $0. В END пишет L.

person James Waldby - jwpat7    schedule 15.10.2012
comment
Сложность любого алгоритма, который начинается с сортировки данных, заключается в том, что вы теряете позиционную информацию о том, какая строка с данным ключом была последней. - person Jonathan Leffler; 15.10.2012
comment
@JonathanLeffler, ты ошибаешься. Я указал -s, чтобы получить стабильную сортировку. Однако я должен был также указать -k1,1 (и, возможно, -t,) и соответствующим образом отредактировать свой ответ. - person James Waldby - jwpat7; 15.10.2012
comment
Я пропустил -s при сканировании вашего ответа - извините. Очевидно, мне пора спать. - person Jonathan Leffler; 15.10.2012