从bash模拟“分组依据”的最佳方法?


231

假设您有一个包含IP地址的文件,每行一个地址:

10.0.10.1
10.0.10.1
10.0.10.3
10.0.10.2
10.0.10.1

您需要一个Shell脚本,该脚本针对每个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而不是那些语言的更好解决方案感兴趣。)

附加信息:

假设源文件为5GB,运行该算法的计算机为4GB。因此,排序不是一种有效的解决方案,读取文件也不止一次。

我喜欢类似散列表的解决方案-任何人都可以对该解决方案进行改进吗?

其他信息#2:

有人问为什么在例如perl中更容易用bash进行操作呢?原因是在我必须执行此操作的机器上,我无法使用perl。这是一台定制的Linux机器,没有我惯用的大多数工具。我认为这是一个有趣的问题。

因此,请不要怪这个问题,如果您不喜欢它,那就忽略它。:-)


我认为bash是这项工作的错误工具。Perl可能是一个更好的解决方案。
弗朗索瓦·沃尔玛兰斯

Answers:


412
sort ip_addresses | uniq -c

这将首先打印计数,但除此之外,它应该正是您想要的。


71
然后您可以通过管道将其“排序-nr”从最高到最低按降序排序。即sort ip_addresses | uniq -c | sort -nr
Brad Parks 2014年

15
sort ip_addresses | uniq -c | sort -nr | awk '{ print $2, $1 }'在第一列中获取IP地址,然后在第二列中计数。
Raghu Dodda '16

另一部分调整:sort -nr -k1,1
Andrzej Martyna

50

快速而肮脏的方法如下:

cat ip_addresses | sort -n | uniq -c

如果需要使用bash中的值,则可以将整个命令分配给bash变量,然后循环遍历结果。

聚苯乙烯

如果省略了sort命令,那么由于uniq只看连续的相同行,您将不会得到正确的结果。


这在效率方面非常相似,您仍然具有二次行为
Vinko Vrsalovic

二次含义O(n ^ 2)?? 那肯定取决于排序算法,不太可能使用这样的bogo-sort。
paxdiablo

好吧,在最佳情况下,它应该是O(n log(n)),比两次通过都差(这是基于琐碎的基于散列的实现所得到的结果)。我应该说“超线性”而不是平方。
Vinko Vrsalovic

而且,OP要求提高效率明智的做法仍然处于同一界限……
Vinko Vrsalovic

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 2014年

1
+1是因为sortuniq最容易进行计数,但是当您需要计算/求和字段值时却无济于事。awk的数组语法非常强大,是在此处进行分组的关键。谢谢!
odony

1
还有一两件事,看的是awk的print功能似乎缩减64个整数32位,所以对于超过2 ^ 31 int类型,你可能想使用printf%.0f替代的格式print
odony

1
寻找具有字符串连接而不是加号的“分组依据”的人将成功替换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 sort命令的实现质量。在内存为128k(即131,072字节)(PDP-11)的计算机上,使用Sort来处理大量数据(认为是AT&T的原始账单数据)。当排序遇到的数据超过预设限制(通常将其调整为接近机器主内存的大小)时,它将对已在主内存中读取的数据进行排序并将其写入临时文件。然后,它对下一个数据块重复该操作。最后,它对那些中间文件执行合并排序。这样,排序就可以处理比机器主内存大许多倍的数据。


好吧,它仍然比哈希计数还差,不是吗?您是否知道如果数据适合内存,将使用哪种排序算法?在数字数据大小写(-n选项)中是否有所不同?
Vinko Vrsalovic

这取决于sort(1)的实现方式。GNU排序(用于Linux发行版)和BSD排序都花了很长的时间才能使用最合适的算法。
Diomidis Spinellis

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中提供更干净的实现吗?
Zizzencs

从来没听说过。您可以使用支持哈希的语言获得更好的实现,其中您需要为我的$ ip(@ips){$ hash {$ ip} = $ hash {$ ip} + 1; },然后只打印键和值。
Vinko Vrsalovic

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

在这里发帖


是的,很棒的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位无符号整数值,用作array的索引。这使用简单的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个地址,但是当我尝试对10 万个地址进行排序时,大约需要整整1秒钟的时间。


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而不是set易于实现。

编辑:我是一个糟糕的读者,所以我回答错了。这是一个带有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作为键的列表,以及它们出现的次数作为其值的列表。


这不算什么。您需要保留分数的字典。

h 不好看这个问题,对不起。我最初对使用字典存储每个IP地址出现的时间有一些了解,但是删除了它,因为,好吧,我对问题的理解并不十分清楚。*尝试正确唤醒
wzzrd

2
有一个itertools.groupby()sorted()OP恰好相结合的功能。
jfs

这是python中的一个很棒的解决方案,不适用于此:-)
Zizzencs'Dec 23'08

-8

如果顺序不重要,则可以省略排序

uniq -c <source_file>

要么

echo "$list" | uniq -c

如果源列表是变量


1
要进一步说明,请从uniq手册页中进行:注意:“ uniq”不会检测到重复的行,除非它们相邻。您可能要先对输入进行排序,或者使用不带“ uniq”的“ sort -u”。
Converter42
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.