在 bash 中对没有 uniq 的列进行排序和计数

Sort and counting a column without uniq in bash

我只想使用 bash 添加第一列的计数,而不使用 uniq,如下所示:

输入:

58311s2727  NC_000082.6 100.00  50  
58311s2727  NC_000083.6 100.00  60
58311s2727  NC_000084.6 100.00  70
58310s2691  NC_000080.6 100.00  30
58310s2691  NC_000081.6 100.00  20
58308s2441  NC_000074.6 100.00  50

输出:

3  58311s2727   NC_000082.6 100.00  50  
3  58311s2727   NC_000083.6 100.00  60
3  58311s2727   NC_000084.6 100.00  70
2  58310s2691   NC_000080.6 100.00  30
2  58310s2691   NC_000081.6 100.00  20
1  58308s2441   NC_000074.6 100.00  50

我试过:

sort input.txt | cut -f1 | uniq -c

但输出不是我想要的。我想知道是否有简单的方法可以解决这个问题。

如果没有 uniq,您将不得不阅读输入两次。在纯 BASH 中有很多方法可以做到这一点,但这是我切换到适当的脚本语言的时候,比如 Python 2:

import codecs
from collections import Counter

filename='...'
encoding='...' # file encoding

counter = Counter()
with codecs.open(filename, 'r', encoding) as fh:
   for line in fh:
       parts = line.split(' ')
       counter[parts[0]] += 1

with codecs.open(filename, 'r', encoding) as fh:
   for line in fh:
       parts = line.split(' ')
       count = counter[parts[0]]
       print '%d%s' % (count, line),

我会在 awk 中执行此操作。但正如 Aaron 所说,它需要读取输入两次,因为你第一次点击特定行时,你不知道还会点击多少次。

$ awk 'NR==FNR{a[]++;next} {print a[],[=10=]}' inputfile inputfile

这是第一次遍历文件,用第一个字段的计数器填充数组。然后它进行第二次,打印每行的计数。

您可以调整 print 语句以满足您的格式要求(也许将其替换为 printf)。


如果您不想使用 awk,并且真的希望它在 bash 中本地工作,您可以使用几个带有 for 循环的单行代码来获得几乎相同的结果:

$ declare -A a
$ while read word therest; do ((a[$word]++)); done < inputfile
$ while read word therest; do printf "%5d\t%s\t%s\n" "${a[$word]}" "$word" "$therest"; done < inputfile

declare -A是必需的,因为$a需要是一个关联数组,每行的第一个单词作为键。另一方面,awk 将每个数组视为关联数组。请注意,此解决方案不会保留您的空格。

对于已排序的输入,您可以简单地使用 awk,捕获具有相同键的行集,并在键更改时打印前一组。处理 EOF 有点麻烦;你必须重复打印。您可以编写一个 awk 函数来进行打印,但对于这么简单的事情来说,这几乎是多余的。

script.awk

 != old_key { if (n_keys > 0) for (i = 0; i < n_keys; i++) print n_keys, saved[i]; n_keys = 0 }
{ saved[n_keys++] = [=10=]; old_key =  }
END { if (n_keys > 0) for (i = 0; i < n_keys; i++) print n_keys, saved[i] }

示例运行

对于样本输入input.txt(已经分组),输出为:

$ awk -f script.awk input.txt
3 58311s2727  NC_000082.6 100.00  50  
3 58311s2727  NC_000083.6 100.00  60
3 58311s2727  NC_000084.6 100.00  70
2 58310s2691  NC_000080.6 100.00  30
2 58310s2691  NC_000081.6 100.00  20
1 58308s2441  NC_000074.6 100.00  50
$

如果要排序,请先排序:

$ sort input.txt | awk -f script.awk
1 58308s2441  NC_000074.6 100.00  50
2 58310s2691  NC_000080.6 100.00  30
2 58310s2691  NC_000081.6 100.00  20
3 58311s2727  NC_000082.6 100.00  50  
3 58311s2727  NC_000083.6 100.00  60
3 58311s2727  NC_000084.6 100.00  70
$

请注意,除其他优点外,这可以处理来自管道的数据,因为它不需要处理文件两次,至少与目前接受的其他解决方案之一不同。它还只在内存中保留与最大公共密钥组中的行一样多的行,因此即使是相当大的文件也不太可能对系统内存造成压力。 (sort 可能比 awk 施加更多的内存负载。)

script2.awk

使用一个函数,有些白space,代码变为:

function dump_keys(    i) {
    if (n_keys > 0)
    {
        for (i = 0; i < n_keys; i++)
            print n_keys, saved[i]
    }
    n_keys = 0
}
 != old_key { dump_keys() }
              { saved[n_keys++] = [=13=]; old_key =  }
END           { dump_keys() }

变量 i 是函数的局部变量(awk 的怪癖)。我可以简单地从参数列表中省略它,因为 i 没有在脚本的其他地方使用。

这会产生与 script.awk 相同的输出。