Как найти строки в файле с одинаковыми значениями для определенных столбцов с помощью команд unix?

У меня есть файл с большим количеством строк. Каждая строка содержит 5 столбцов, разделенных символами табуляции. Я хочу найти все строки, которые имеют одинаковое значение для первых 4 столбцов, но имеют разные значения для 5-го столбца.

name     age    address    phone    city
eric      5      add1      1234     City1
jerry     5      add1      1234     City2
eric      5      add1      1234     City3
eric      5      add1      1234     City4
jax       5      add1      1234     City5
jax       5      add1      1234     City6
niko      5      add1      1234     City7

Результат для этой таблицы должен быть

 eric      5      add1      1234     City1
 eric      5      add1      1234     City3
 eric      5      add1      1234     City4
 jax       5      add1      1234     City5
 jax       5      add1      1234     City6

Я пытался использовать uniq -u -f4 после sort, но это игнорирует первые 4 поля, которые в этом случае вернут все строки.


person tourniquet_grab    schedule 15.10.2015    source источник


Ответы (1)


Я был бы склонен использовать awk для этого.

script.awk

{ x = count[$1,$2,$3,$4]++; line[$1,$2,$3,$4,x] = $0 }
END {   for (key in count)
        {
            kc = count[key]
            if (kc > 1)
            {
                for (i = 0; i < kc; i++)
                {
                    print line[key,i]
                }
            }
        }
    }

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

Пробный запуск

$ awk -f script.awk data
jax       5      add1      1234     City5
jax       5      add1      1234     City6
eric      5      add1      1234     City1
eric      5      add1      1234     City3
eric      5      add1      1234     City4
$

Обратите внимание, что при этом ключи генерируются в другом порядке, в котором они появляются в файле (первая запись eric, 5, add1, 1234 предшествует первой записи jax, 5, add1, 1234).

Это можно было бы решить, если это необходимо сделать.

script2.awk

{   x = count[$1,$2,$3,$4]++;
    line[$1,$2,$3,$4,x] = $0
    if (x == 0)
        seq[n++] = $1 SUBSEP $2 SUBSEP $3 SUBSEP $4
}
END {   for (s = 0; s < n; s++)
        {
            key = seq[s]
            kc = count[key]
            if (kc > 1)
            {
                for (i = 0; i < kc; i++)
                {
                    print line[key,i]
                }
            }
        }
    }

SUBSEP — это символ, используемый для разделения компонентов многоэлементного ключа, поэтому присваивание seq[n++] записывает значение, используемое в качестве индекса в count[$1,$2,$3,$4]. Массив seq записывает каждый ключ (первые четыре столбца) в том порядке, в котором они появляются. Последовательный проход по этому массиву дает ключи в том порядке, в котором появляется первая запись.

Пробный запуск

$ awk -f script2.awk data
eric      5      add1      1234     City1
eric      5      add1      1234     City3
eric      5      add1      1234     City4
jax       5      add1      1234     City5
jax       5      add1      1234     City6
$

Предварительная обработка данных для экономии памяти и ускорения обработки

Приведенный выше код хранит много данных в памяти. Он имеет полную копию каждой строки в файлах данных; у него есть ключ с первыми четырьмя полями; у него есть еще один ключ с четырьмя полями плюс целое число. Для большинства практических целей это 3 копии данных. Если файлы даты большие, это может быть проблемой. Однако, учитывая, что в выборке данных строка jerry появляется в середине строк eric, невозможно добиться большего успеха, если только данные не отсортированы в первую очередь. Тогда вы знаете, что все связанные строки находятся в файле вместе, и вы можете обрабатывать его намного проще.

script3.awk

{   
    new_key = $1 SUBSEP $2 SUBSEP $3 SUBSEP $4
    if (new_key == old_key)
    {
        if (old_line != "") { print old_line; old_line = "" }
        print $0
    }
    else
    {
        old_line = $0
        old_key = new_key
    }
}

Пробный запуск

$ sort data | awk -f script3.awk
eric      5      add1      1234     City1
eric      5      add1      1234     City3
eric      5      add1      1234     City4
jax       5      add1      1234     City5
jax       5      add1      1234     City6
$

Конечно, это совпадение, что eric предшествует jax в алфавитной последовательности; при сортировке вы теряете исходную последовательность данных. Но скрипт script3.awk хранит в памяти максимум два ключа и одну строку, что не будет нагружать память. Добавление времени сортировки по-прежнему, вероятно, дает вам измеримую экономию по сравнению с исходным механизмом обработки.

Если первоначальный заказ критичен, вам придется проделать гораздо больше работы. Я думаю, что это включает в себя нумерацию каждой строки в исходном файле, сортировку с использованием номера строки в качестве пятого ключа после того, как первые четыре ключа группируют одни и те же ключи вместе, а затем определяют каждую группу строк с теми же четырьмя значениями ключа с тем же номером строки , затем снова отсортируйте по номеру группы и порядковому номеру внутри группы и передайте это модифицированной версии обработки в сценарии script3.awk. Но это все же может быть лучше, чем оригинал, если файлы находятся в диапазоне гигабайт. Однако единственный способ убедиться в этом — провести измерения на примерах реалистичных размеров.

Например:

nl data |
sort -k2,2 -k3,3 -k4,4 -k5,5 -k1,1n |
awk '{ new_key = $2 SUBSEP $3 sUBSEP $4 SUBSEP $5
       if (old_key != new_key) { grp_seq = $1 }
       print grp_seq, $0
       old_key = new_key
     }' |
sort -k1,1n -k2,2n

Это сильно порождает:

1      1    name     age    address    phone    city
2      2    eric      5      add1      1234     City1
2      4    eric      5      add1      1234     City3
2      5    eric      5      add1      1234     City4
3      3    jerry     5      add1      1234     City2
6      6    jax       5      add1      1234     City5
6      7    jax       5      add1      1234     City6
8      8    niko      5      add1      1234     City7

Затем вы можете применить модифицированную версию script3.awk, которая игнорирует $1 и $2 для получения желаемого результата. Или вы можете запустить показанный вывод через программу, которая удаляет два ведущих столбца.

person Jonathan Leffler    schedule 15.10.2015
comment
Я нашел способ сделать это с помощью скрипта Ruby, но это здорово. Спасибо! Будет ли это работать с действительно большими файлами (около 10 миллионов строк)? Команда долго не возвращается, если файл действительно большой. Я перенаправляю вывод в новый файл. - person tourniquet_grab; 15.10.2015
comment
Он должен хранить в памяти копию всего файла, плюс дубликат первых четырех столбцов, плюс некоторые административные накладные расходы. Учитывая, что данные представлены не по порядку (jerry появляется в середине строк eric), трудно сделать лучше. Однако, если вы предварительно обработали данные (sort; это не должно быть более сложным), вы можете знать, что связанные строки все вместе во входных данных, и поэтому вам не нужно сохранять все. Это может быть неплохой экономией, если вы имеете дело с файлами размером в несколько мегабайт или больше. - person Jonathan Leffler; 15.10.2015