在 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
相同的输出。
我只想使用 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
相同的输出。