假设你有一个包含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机器,没有我使用过的大多数工具。我认为这是一个有趣的问题。

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


当前回答

解决方案(分组如mysql)

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

结果

3249  googleplus
4211 linkedin
5212 xing
7928 facebook

其他回答

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

我知道你在Bash中寻找一些东西,但如果其他人可能在Python中寻找一些东西,你可能会考虑这样做:

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

由于集合中的值在默认情况下是唯一的,而Python在这方面非常擅长,因此您可能会在这里赢得一些东西。我还没有测试代码,所以它可能有漏洞,但这可能会让你明白。如果你想要计数出现的次数,使用字典而不是集合很容易实现。

编辑: 我不擅长阅读,所以我答错了。这里有一个字典片段,可以计算发生的次数。

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的列表作为键,它们出现的次数作为值。

sort ip_addresses | uniq -c

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

将数据导入sqlite db并使用sql语法(只是另一个想法)。 我知道这对于这个例子来说太多了,但是对于有多个文件(表)的复杂查询是有用的

#!/bin/bash
trap clear_db EXIT
clear_db(){ rm -f "mydb$$"; }

# add header to input_file (IP)
INPUT_FILE=ips.txt

# import file into db
sqlite3 -csv mydb$$ ".import ${INPUT_FILE} mytable"

# using sql statements on table 'mytable' 
sqlite3 mydb$$ -separator " "  "SELECT IP, COUNT(*) FROM mytable GROUP BY IP;"

10.0.10.1 3
10.0.10.2 1
10.0.10.3 1

您可能可以使用文件系统本身作为哈希表。伪代码如下:

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

最后,您所需要做的就是遍历所有文件,并在其中打印文件名和编号。或者,您可以每次在文件中附加一个空格或换行符,而不是保持计数,最后只需查看文件大小(以字节为单位)。