假设你有一个包含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:

有些人问我为什么要在bash中做,而在例如perl中更容易。原因是,在机器上,我必须这样做perl是不可为我。这是一台定制的linux机器,没有我使用过的大多数工具。我认为这是一个有趣的问题。

所以,请不要责怪这个问题,如果你不喜欢它,就忽略它。:-)


当前回答

GROUP BY under bash

关于这个SO线程,根据不同的需求有一些不同的答案。

1. 将IP计数为SO请求(按IP地址分组)。

由于IP很容易转换为单个整数,对于小串地址,如果您需要多次重复这种操作,使用纯bash函数可能会更有效!

纯粹的bash(没有fork!)

有一种方法,使用bash函数。这条路非常快,因为没有叉子!

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

在我的主机上,这样做比使用fork快得多,最多可以使用大约1000个地址,但当我尝试排序并计数10,000个地址时,大约需要整整1秒钟。

2. GROUP BY duplicate(文件内容)

通过使用校验和,你可以在某个地方标识重复的文件:

find . -type f -exec sha1sum {} + |
    sort |
        sed '
          :a;
          $s/^[^ ]\+ \+//;
          N;
          s/^\([^ ]\+\) \+\([^ ].*\)\n\1 \+\([^ ].*\)$/\1 \2\o11\3/;
          ta;
          s/^[^ ]\+ \+//;
          P;
          D;
          ba
    '

这将打印所有副本,按行,以制表($'\t'或八进制011 ou可以更改/\1 \2\o11\3/;By /\1 \2|\3/;使用|作为分隔符)。

./b.txt   ./e.txt
./a.txt   ./c.txt    ./d.txt

可以写成(以|为分隔符):

find . -type f -exec sha1sum {} + | sort | sed ':a;$s/^[^ ]\+ \+//;N;
  s/^\([^ ]\+\) \+\([^ ].*\)\n\1 \+\([^ ].*\)$/\1 \2|\3/;ta;s/^[^ ]\+ \+//;P;D;ba'

纯粹的bash方式

通过使用nameref,你可以构建一个包含所有副本的bash数组:

declare -iA sums='()'
while IFS=' ' read -r sum file ;do
    declare -n list=_LST_$sum
    list+=("$file")
    sums[$sum]+=1
done < <(
    find . -type f -exec sha1sum {} +
)

从那里,你有一堆数组保存所有重复的文件名作为分离的元素:

for i in ${!sums[@]};do
     declare -n list=_LST_$i
     printf "%d %d %s\n" ${sums[$i]} ${#list[@]} "${list[*]}"
done

这可能会输出如下内容:

2 2 ./e.txt ./b.txt
3 3 ./c.txt ./a.txt ./d.txt

md5sum (${sum [$shasum]})匹配数组${_LST_ShAsUm[@]}中元素的计数。

for i in ${!sums[@]};do
    declare -n list=_LST_$i
    echo ${list[@]@A}
done
declare -a _LST_22596363b3de40b06f981fb85d82312e8c0ed511=([0]="./e.txt" [1]="./b.txt")
declare -a _LST_f572d396fae9206628714fb2ce00f72e94f2258f=([0]="./c.txt" [1]="./a.txt" [2]="./d.txt")

注意,这个方法可以处理文件名中的空格和特殊字符!

3.GROUP BY表中的列

由于匿名者提供了使用awk的有效示例,这里是一个纯bash解决方案。

所以你想要总结第3列到最后一列,并按第1列和第2列分组,table.txt看起来像这样

我们| 1000 | 2000 | 我们| 1000 | 2000 | B 我们C | 1000 | 2000 | 英国| 1 | 1000 | 2000 英国| 1 | 1000 | 2000 | 3000年 英国| 1 | 1000 | 2000 | 3000 | 4000

对于不太大的表格,你可以:

myfunc() {
    local -iA restabl='()';
    local IFS=+
    while IFS=\| read -ra ar; do
        restabl["${ar[0]}|${ar[1]}"]+="${ar[*]:2}"
    done
    for i in ${!restabl[@]} ;do
        printf '%s|%s\n' "$i" "${restabl[$i]}"
    done
}

可以输出如下内容:

myfunc <table.txt 
UK|1|19000
US|A|3000
US|C|3000
US|B|3000

对表进行排序:

myfunc() {
    local -iA restabl='()';
    local IFS=+ sorted=()
    while IFS=\| read -ra ar; do
        sorted[64#${ar[0]}${ar[1]}]="${ar[0]}|${ar[1]}"
        restabl["${ar[0]}|${ar[1]}"]+="${ar[*]:2}"
    done
    for i in ${sorted[@]} ;do
        printf '%s|%s\n' "$i" "${restabl[$i]}"
    done
}

必须返回:

myfunc <table 
UK|1|19000
US|A|3000
US|B|3000
US|C|3000

其他回答

GROUP BY under bash

关于这个SO线程,根据不同的需求有一些不同的答案。

1. 将IP计数为SO请求(按IP地址分组)。

由于IP很容易转换为单个整数,对于小串地址,如果您需要多次重复这种操作,使用纯bash函数可能会更有效!

纯粹的bash(没有fork!)

有一种方法,使用bash函数。这条路非常快,因为没有叉子!

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

在我的主机上,这样做比使用fork快得多,最多可以使用大约1000个地址,但当我尝试排序并计数10,000个地址时,大约需要整整1秒钟。

2. GROUP BY duplicate(文件内容)

通过使用校验和,你可以在某个地方标识重复的文件:

find . -type f -exec sha1sum {} + |
    sort |
        sed '
          :a;
          $s/^[^ ]\+ \+//;
          N;
          s/^\([^ ]\+\) \+\([^ ].*\)\n\1 \+\([^ ].*\)$/\1 \2\o11\3/;
          ta;
          s/^[^ ]\+ \+//;
          P;
          D;
          ba
    '

这将打印所有副本,按行,以制表($'\t'或八进制011 ou可以更改/\1 \2\o11\3/;By /\1 \2|\3/;使用|作为分隔符)。

./b.txt   ./e.txt
./a.txt   ./c.txt    ./d.txt

可以写成(以|为分隔符):

find . -type f -exec sha1sum {} + | sort | sed ':a;$s/^[^ ]\+ \+//;N;
  s/^\([^ ]\+\) \+\([^ ].*\)\n\1 \+\([^ ].*\)$/\1 \2|\3/;ta;s/^[^ ]\+ \+//;P;D;ba'

纯粹的bash方式

通过使用nameref,你可以构建一个包含所有副本的bash数组:

declare -iA sums='()'
while IFS=' ' read -r sum file ;do
    declare -n list=_LST_$sum
    list+=("$file")
    sums[$sum]+=1
done < <(
    find . -type f -exec sha1sum {} +
)

从那里,你有一堆数组保存所有重复的文件名作为分离的元素:

for i in ${!sums[@]};do
     declare -n list=_LST_$i
     printf "%d %d %s\n" ${sums[$i]} ${#list[@]} "${list[*]}"
done

这可能会输出如下内容:

2 2 ./e.txt ./b.txt
3 3 ./c.txt ./a.txt ./d.txt

md5sum (${sum [$shasum]})匹配数组${_LST_ShAsUm[@]}中元素的计数。

for i in ${!sums[@]};do
    declare -n list=_LST_$i
    echo ${list[@]@A}
done
declare -a _LST_22596363b3de40b06f981fb85d82312e8c0ed511=([0]="./e.txt" [1]="./b.txt")
declare -a _LST_f572d396fae9206628714fb2ce00f72e94f2258f=([0]="./c.txt" [1]="./a.txt" [2]="./d.txt")

注意,这个方法可以处理文件名中的空格和特殊字符!

3.GROUP BY表中的列

由于匿名者提供了使用awk的有效示例,这里是一个纯bash解决方案。

所以你想要总结第3列到最后一列,并按第1列和第2列分组,table.txt看起来像这样

我们| 1000 | 2000 | 我们| 1000 | 2000 | B 我们C | 1000 | 2000 | 英国| 1 | 1000 | 2000 英国| 1 | 1000 | 2000 | 3000年 英国| 1 | 1000 | 2000 | 3000 | 4000

对于不太大的表格,你可以:

myfunc() {
    local -iA restabl='()';
    local IFS=+
    while IFS=\| read -ra ar; do
        restabl["${ar[0]}|${ar[1]}"]+="${ar[*]:2}"
    done
    for i in ${!restabl[@]} ;do
        printf '%s|%s\n' "$i" "${restabl[$i]}"
    done
}

可以输出如下内容:

myfunc <table.txt 
UK|1|19000
US|A|3000
US|C|3000
US|B|3000

对表进行排序:

myfunc() {
    local -iA restabl='()';
    local IFS=+ sorted=()
    while IFS=\| read -ra ar; do
        sorted[64#${ar[0]}${ar[1]}]="${ar[0]}|${ar[1]}"
        restabl["${ar[0]}|${ar[1]}"]+="${ar[*]:2}"
    done
    for i in ${sorted[@]} ;do
        printf '%s|%s\n' "$i" "${restabl[$i]}"
    done
}

必须返回:

myfunc <table 
UK|1|19000
US|A|3000
US|B|3000
US|C|3000
cat ip_addresses | sort | uniq -c | sort -nr | awk '{print $2 " " $1}'

这个命令将提供您想要的输出

awk + sort(带版本排序标志)的组合可能是最快的(如果你的环境有awk的话):

echo "${input...}" |

{m,g}awk '{ __[$+_]++ } END { for(_ in __) { print "",+__[_],_ } }' FS='^$' OFS='\t' | 

gsort -t$'\t' -k 3,3 -V

只有后GROUP-BY汇总行被发送到排序实用程序——与毫无理由地对输入行进行预先排序相比,这是一种系统密集型排序。

对于小输入,例如少于1000行左右,只需直接排序|uniq -c它。

    3   10.0.10.1
    1   10.0.10.2
    1   10.0.10.3

解决方案(分组如mysql)

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

结果

3249  googleplus
4211 linkedin
5212 xing
7928 facebook

又快又脏的方法如下:

Cat ip_addresses | sort -n | uniq -c

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

PS

如果省略sort命令,就不会得到正确的结果,因为uniq只查看连续的相同行。