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

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


当前回答

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

其他回答

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

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

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

典型的解决方案是另一位受访者提到的:

sort | uniq -c

它比用Perl或awk编写的代码更短、更简洁。

You write that you don't want to use sort, because the data's size is larger than the machine's main memory size. Don't underestimate the implementation quality of the Unix sort command. Sort was used to handle very large volumes of data (think the original AT&T's billing data) on machines with 128k (that's 131,072 bytes) of memory (PDP-11). When sort encounters more data than a preset limit (often tuned close to the size of the machine's main memory) it sorts the data it has read in main memory and writes it into a temporary file. It then repeats the action with the next chunks of data. Finally, it performs a merge sort on those intermediate files. This allows sort to work on data many times larger than the machine's main memory.

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

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

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