Лучший способ смоделировать «group by» из Bash?


231

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

10.0.10.1
10.0.10.1
10.0.10.3
10.0.10.2
10.0.10.1

Вам нужен сценарий оболочки, который подсчитывает для каждого IP-адреса, сколько раз он появляется в файле. Для предыдущего ввода вам понадобится следующий вывод:

10.0.10.1 3
10.0.10.2 1
10.0.10.3 1

Один из способов сделать это:

cat ip_addresses |uniq |while read ip
do
    echo -n $ip" "
    grep -c $ip ip_addresses
done

Однако это действительно далеко не эффективно.

Как бы вы решили эту проблему более эффективно, используя bash?

(Следует добавить: я знаю, что это можно решить с помощью perl или awk, меня интересует лучшее решение для bash, а не для этих языков.)

ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ:

Предположим, что исходный файл имеет 5 ГБ, а машина, на которой работает алгоритм, имеет 4 ГБ. Так что сортировка не является эффективным решением и не читает файл более одного раза.

Мне понравилось решение, похожее на хеш-таблицу - кто-нибудь может предложить улучшения для этого решения?

ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ № 2:

Некоторые люди спрашивали, зачем мне это делать в bash, когда это проще, например, в perl. Причина в том, что на машине, которую я должен был сделать, этот Perl был недоступен для меня. Это была специально созданная машина Linux без большинства инструментов, к которым я привык. И я думаю, что это была интересная проблема.

Поэтому, пожалуйста, не вините вопрос, просто проигнорируйте его, если он вам не нравится. :-)


Я думаю, что bash - это не тот инструмент для работы. Perl, вероятно, будет лучшим решением.
Франсуа Волмаранс

Ответы:


412
sort ip_addresses | uniq -c

Сначала будет напечатан счетчик, но в остальном он должен быть именно тем, что вы хотите.


71
который вы можете затем направить на «sort -nr», чтобы отсортировать в порядке убывания от наибольшего к наименьшему числу. то естьsort ip_addresses | uniq -c | sort -nr
Брэд Паркс

15
И sort ip_addresses | uniq -c | sort -nr | awk '{ print $2, $1 }'получить IP-адрес в первом столбце и считать во втором.
Рагху Додда

Еще один твик для сортировки:sort -nr -k1,1
Анджей Мартина

50

Быстрый и грязный метод заключается в следующем:

cat ip_addresses | sort -n | uniq -c

Если вам нужно использовать значения в bash, вы можете назначить всю команду переменной bash, а затем просмотреть результаты.

PS

Если команда сортировки опущена, вы не получите правильные результаты, так как uniq просматривает только последовательные идентичные строки.


Это очень похоже на эффективность, у вас все еще есть квадратичное поведение
Vinko Vrsalovic

Квадратичное значение O (n ^ 2) ?? Конечно, это будет зависеть от алгоритма сортировки, вряд ли он будет использовать такую ​​сортировку.
paxdiablo

Ну, в лучшем случае это будет O (n log (n)), что хуже, чем два прохода (это то, что вы получаете с помощью тривиальной реализации на основе хеша). Я должен был сказать «суперлинейный» вместо квадратичного.
Винко Врсалович

И это все еще в том же пределе, что то, что ОП попросило повысить эффективность в разумных
пределах

11
uuoc, бесполезное использование кошки

22

для суммирования нескольких полей на основе группы существующих полей используйте приведенный ниже пример: (замените $ 1, $ 2, $ 3, $ 4 в соответствии с вашими требованиями)

cat file

US|A|1000|2000
US|B|1000|2000
US|C|1000|2000
UK|1|1000|2000
UK|1|1000|2000
UK|1|1000|2000

awk 'BEGIN { FS=OFS=SUBSEP="|"}{arr[$1,$2]+=$3+$4 }END {for (i in arr) print i,arr[i]}' file

US|A|3000
US|B|3000
US|C|3000
UK|1|9000

2
+1, потому что показывает, что делать, когда нужен не только счет
user829755

1
+1 потому что sortи uniqпроще всего делать подсчет, но не помогает, когда вам нужно вычислить / суммировать значения полей. Синтаксис массива awk очень мощный и ключевой для группировки здесь. Спасибо!
Одоны

1
еще одна вещь, смотреть, что AWK в printфункции , кажется, 64 бит вниз по шкале целых чисел до 32 бит, так что для ИНТ значений , превышающих 2 ^ 31 вы можете использовать printfс %.0fформатом вместо printтам
odony

1
Люди, ищущие «group by» с конкатенацией строк вместо добавления чисел, заменили бы, arr[$1,$2]+=$3+$4например, на arr[$1,$2]=(arr[$1,$2] $3 "," $4). I needed this to provide a grouped-by-package list of files (two columns only) and used: arr [$ 1] = (arr [$ 1] $ 2) `с успехом.
Стефан Гурихон

20

Каноническое решение упомянуто другим респондентом:

sort | uniq -c

Это короче и более кратко, чем то, что можно написать на Perl или awk.

Вы пишете, что не хотите использовать сортировку, потому что размер данных больше размера основной памяти машины. Не стоит недооценивать качество реализации команды сортировки Unix. Сортировка использовалась для обработки очень больших объемов данных (например, исходных данных биллинга AT & T) на машинах с 128 КБ (это 131 072 байта) памяти (PDP-11). Когда сортировка встречает больше данных, чем предварительно установленный предел (часто настраиваемый близко к размеру основной памяти машины), она сортирует данные, прочитанные в основной памяти, и записывает их во временный файл. Затем он повторяет действие со следующими порциями данных. Наконец, он выполняет сортировку слиянием этих промежуточных файлов. Это позволяет сортировке работать с данными, во много раз превышающими основную память машины.


Ну, это все еще хуже, чем количество хэшей, нет? Знаете ли вы, какой алгоритм сортировки использует сортировка, если данные помещаются в память? Отличается ли оно в случае числовых данных (опция -n)?
Винко Врсалович

Это зависит от того, как сортировка (1) реализована. Сортировка GNU (используется в дистрибутивах Linux) и сортировка BSD идут на все, чтобы использовать наиболее подходящий алгоритм.
Диомидис Спинеллис

9
cat ip_addresses | sort | uniq -c | sort -nr | awk '{print $2 " " $1}'

эта команда даст вам желаемый результат


4

Похоже, вам нужно либо использовать большой объем кода для имитации хэшей в bash, чтобы получить линейное поведение, либо придерживаться квадратичных суперлинейных версий.

Среди этих версий решение saua является лучшим (и самым простым):

sort -n ip_addresses.txt | uniq -c

Я нашел http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-11/0118.html . Но это ужасно чертовски ...


Я согласен. На данный момент это лучшее решение, и подобные решения возможны в perl и awk. Кто-нибудь может обеспечить более чистую реализацию в bash?
Жижценц

Не то, что я знаю о. Вы можете получить лучшую реализацию на языках, поддерживающих хэши, где вы делаете для моего $ ip (@ips) {$ hash {$ ip} = $ hash {$ ip} + 1; }, а затем просто распечатайте ключи и значения.
Винко Врсалович

4

Решение (сгруппировано как mysql)

grep -ioh "facebook\|xing\|linkedin\|googleplus" access-log.txt | sort | uniq -c | sort -n

результат

3249  googleplus
4211 linkedin
5212 xing
7928 facebook

3

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

for every entry in the ip address file; do
  let addr denote the ip address;

  if file "addr" does not exist; then
    create file "addr";
    write a number "0" in the file;
  else 
    read the number from "addr";
    increase the number by 1 and write it back;
  fi
done

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


3

Я чувствую, awk ассоциативный массив также удобен в этом случае

$ awk '{count[$1]++}END{for(j in count) print j,count[j]}' ips.txt

Группа по почте здесь


Yepp, отличное решение awk, но awk просто не был доступен на машине, на которой я делал это.
Zizzencs

1

Большинство других решений считают дубликаты. Если вам действительно нужно сгруппировать пары ключ-значение, попробуйте это:

Вот мой пример данных:

find . | xargs md5sum
fe4ab8e15432161f452e345ff30c68b0 a.txt
30c68b02161e15435ff52e34f4fe4ab8 b.txt
30c68b02161e15435ff52e34f4fe4ab8 c.txt
fe4ab8e15432161f452e345ff30c68b0 d.txt
fe4ab8e15432161f452e345ff30c68b0 e.txt

Это напечатает пары ключ-значение, сгруппированные по контрольной сумме md5.

cat table.txt | awk '{print $1}' | sort | uniq  | xargs -i grep {} table.txt
30c68b02161e15435ff52e34f4fe4ab8 b.txt
30c68b02161e15435ff52e34f4fe4ab8 c.txt
fe4ab8e15432161f452e345ff30c68b0 a.txt
fe4ab8e15432161f452e345ff30c68b0 d.txt
fe4ab8e15432161f452e345ff30c68b0 e.txt

1

чистый (без вилки!)

Есть способ, используя функция . Этот путь очень быстрый, так как нет вилки! ...

... Пока куча ip адресов остается маленькой !

countIp () { 
    local -a _ips=(); local _a
    while IFS=. read -a _a ;do
        ((_ips[_a<<24|${_a[1]}<<16|${_a[2]}<<8|${_a[3]}]++))
    done
    for _a in ${!_ips[@]} ;do
        printf "%.16s %4d\n" \
          $(($_a>>24)).$(($_a>>16&255)).$(($_a>>8&255)).$(($_a&255)) ${_ips[_a]}
    done
}

Примечание. IP-адреса преобразуются в 32-разрядное целое число без знака, используемое в качестве индекса для массива . Это использует простые массивы bash , а не ассоциативный массив (который дороже)!

time countIp < ip_addresses 
10.0.10.1    3
10.0.10.2    1
10.0.10.3    1
real    0m0.001s
user    0m0.004s
sys     0m0.000s

time sort ip_addresses | uniq -c
      3 10.0.10.1
      1 10.0.10.2
      1 10.0.10.3
real    0m0.010s
user    0m0.000s
sys     0m0.000s

На моем хосте это происходит намного быстрее, чем при использовании вилок, примерно до 1000 адресов, но занимает около 1 секунды, когда я попытаюсь отсортировать не более 10 тысяч адресов.


0

Я бы сделал это так:

perl -e 'while (<>) {chop; $h{$_}++;} for $k (keys %h) {print "$k $h{$k}\n";}' ip_addresses

но Uniq может работать для вас.


Как я уже говорил в оригинальном посте, Perl не вариант. Я знаю, что это легко в Perl, нет проблем с этим :-)
Zizzencs

0

Я понимаю, что вы ищете что-то в Bash, но в случае, если кто-то еще ищет что-то в Python, вы можете рассмотреть это:

mySet = set()
for line in open("ip_address_file.txt"):
     line = line.rstrip()
     mySet.add(line)

Поскольку значения в наборе уникальны по умолчанию, и Python довольно хорош в этом, вы можете выиграть что-то здесь. Я не проверял код, поэтому он может быть ошибочным, но это может привести вас к этому. И если вы хотите считать события, легко использовать dict вместо набора.

Редактировать: я паршивый читатель, поэтому я ответил неправильно. Вот фрагмент кода с указанием количества событий.

mydict = {}
for line in open("ip_address_file.txt"):
    line = line.rstrip()
    if line in mydict:
        mydict[line] += 1
    else:
        mydict[line] = 1

Словарь mydict теперь содержит список уникальных IP-адресов в качестве ключей и количество раз, которое они встречались в качестве их значений.


это ничего не значит. вам нужен дикт, который ведет счет.

Doh. Плохое чтение вопроса, извините. Первоначально у меня было кое-что об использовании dict для хранения количества раз, когда каждый IP-адрес встречался, но я удалил его, потому что, ну, я не очень хорошо прочитал вопрос. * пытается правильно проснуться
wzzrd

2
Существует, itertools.groupby()который в сочетании с sorted()делает именно то, что просит OP.
JFS

Это отличное решение в Python, которое не было доступно для этого :-)
Zizzencs

-8

Сортировка может быть опущена, если порядок не имеет значения

uniq -c <source_file>

или

echo "$list" | uniq -c

если список источников является переменной


1
Чтобы уточнить это, на странице руководства uniq: Примечание: «uniq» не обнаруживает повторяющиеся строки, если они не являются смежными. Вы можете сначала отсортировать ввод или использовать sort -u без uniq.
converter42
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.