来自 shell 的 GROUP BY/SUM

Posted

技术标签:

【中文标题】来自 shell 的 GROUP BY/SUM【英文标题】:GROUP BY/SUM from shell 【发布时间】:2012-05-04 10:01:10 【问题描述】:

我有一个包含如下数据的大文件:

a 23
b 8
a 22
b 1

我希望能够得到这个:

a 45
b 9

我可以先对该文件进行排序,然后在 Python 中扫描该文件一次。有什么好的直接命令行方式来执行此操作?

【问题讨论】:

【参考方案1】:

编辑:现代 (GNU/Linux) 解决方案,正如几年前在 cmets 中提到的 ;-)。

awk '
    arr[$1]+=$2
   
   END 
     for (key in arr) printf("%s\t%s\n", key, arr[key])
   ' file \
   | sort -k1,1

最初发布的解决方案,基于旧的 Unix sort 选项:

awk '
    arr[$1]+=$2
   
   END 
     for (key in arr) printf("%s\t%s\n", key, arr[key])
   ' file \
   | sort +0n -1

我希望这会有所帮助。

【讨论】:

这些参数究竟对排序有什么作用?我在手册页中看不到它们,调用页面让我感到困惑。 现代版本的排序更喜欢-k 语法来指定排序键:sort -nk1,1 而不是sort +0n -1。但是既然键是字母,那你为什么还要指定-n呢? @EricR:+0n -1 是老式的 -n -k1,1:按第一个(空格分隔)字段进行数字排序。 您也可以让 awk 进行排序:asorti(arr,keys); for (i in keys) printf "%s\t%s\n", keys[i], arr[keys[i]]); @MarkReed 你可以,但是如果想要扩展到非常大的输入(特别是如果这些输入大于可用内存时,GNU 排序将是更有效的方法;在这种情况下,GNU 排序将分区为临时文件)【参考方案2】:

这里不需要 awk,甚至不需要排序——如果你有 Bash 4.0,你可以使用关联数组:

#!/bin/bash
declare -A values
while read key value; do
  values["$key"]=$(( $value + $values[$key]:-0 ))
done
for key in "$!values[@]"; do
  printf "%s %s\n" "$key" "$values[$key]"
done

...或者,如果您首先对文件进行排序(这将更节省内存;GNU sort 能够对大于内存的文件进行排序,这是一个天真的脚本——无论是在 awk、python 还是 shell -- 通常不会),您可以通过在旧版本中工作的方式执行此操作(我希望以下内容可以通过 bash 2.0 工作):

#!/bin/bash
read cur_key cur_value
while read key value; do
  if [[ $key = "$cur_key" ]] ; then
    cur_value=$(( cur_value + value ))
  else
    printf "%s %s\n" "$cur_key" "$cur_value"
    cur_key="$key"
    cur_value="$value"
  fi
done
printf "%s %s\n" "$cur_key" "$cur_value"

【讨论】:

见鬼,上面的一些最低限度的修改可以在 vanilla Bourne 中工作,不需要 bash。while read key value; do if [ "$key" = "$cur_key" ]; then cur_value=`expr $cur_value + $value`; else echo "$cur_key $cur_value"; cur_key="$key"; cur_value="$value"; fi; done; echo "$cur_key $cur_value" @MarkReed,确实如此,尽管运行expr 的子shell 的性能影响足以使POSIX sh $(( )) 扩展会更好; (( )) 是 bash 扩展,$(( )) 符合标准;它只是需要 expr 的 1991 年之前的 POSIX 标准 Bourne sh。 另一种(特定于 Bash,更简洁)的方式来表达第一个示例中的附加值:(( values["$key"] += value )) 注意:不需要 default-0。【参考方案3】:

这个 Perl 单行似乎可以完成这项工作:

perl -nle '($k, $v) = split; $s$k += $v; END $, = " "; foreach $k (sort keys %s) print $k, $s$k' inputfile

【讨论】:

【参考方案4】:

这可以通过以下单线轻松实现:

cat /path/to/file | termsql "SELECT col0, SUM(col1) FROM tbl GROUP BY col0"

或者。

termsql -i /path/to/file "SELECT col0, SUM(col1) FROM tbl GROUP BY col0"

这里使用了一个 Python 包,termsql,它是 SQLite 的包装器。注意,目前还不能上传到PyPI,也只能全系统安装(setup.py有点破),比如:

pip install --user https://github.com/tobimensch/termsql/archive/master.zip

更新

2020年1.0版终于上传到PyPI,所以pip install --user termsql可以用了。

【讨论】:

我喜欢 termsql 工具!它可以很方便!【参考方案5】:

一种使用perl的方式:

perl -ane '
    next unless @F == 2; 
    $h $F[0]  += $F[1]; 
    END  
        printf qq[%s %d\n], $_, $h $_  for sort keys %h;
    
' infile

infile的内容:

a 23
b 8
a 22
b 1

输出:

a 45
b 9

【讨论】:

【参考方案6】:

使用 GNU awk(版本低于 4):

WHINY_USERS= awk 'END 
  for (E in a)
    print E, a[E]
    
 a[$1] += $2 ' infile

使用 GNU awk >= 4:

awk 'END 
  PROCINFO["sorted_in"] = "@ind_str_asc"
  for (E in a)
    print E, a[E]
    
 a[$1] += $2 ' infile

【讨论】:

我似乎晚了将近三年才偶然发现这一点。 WHINY_USERS 变量到底有什么作用? 对asciibetical order中的数组键进行排序。 链接死了,这个更有可能还活着:***.com/q/11697556/476716【参考方案7】:

使用sort + awk 组合可以尝试关注,而不创建数组。

sort -k1 Input_file | 
awk '
  prev!=$1 && prev
    print prev,(prevSum?prevSum:"N/A")
    prev=prevSum=""
  
  
    prev=$1
    prevSum+=$2
  
  END
    if(prev)
       print prev,(prevSum?prevSum:"N/A")
    
'

说明:为上述添加详细说明。

sort -k1 file1 |                          ##Using sort command to sort Input_file by 1st field and sending output to awk as an input.
awk '                                     ##Starting awk program from here.
  prev!=$1 && prev                       ##Checking condition prev is NOT equal to first field and prev is NOT NULL.
    print prev,(prevSum?prevSum:"N/A")    ##Printing prev and prevSum(if its NULL then print N/A).
    prev=prevSum=""                       ##Nullify prev and prevSum here.
  
  
    prev=$1                               ##Assigning 1st field to prev here.
    prevSum+=$2                           ##Adding 2nd field to prevSum.
  
  END                                    ##Starting END block of this awk program from here.
    if(prev)                             ##Checking condition if prev is NOT NULL then do following.
       print prev,(prevSum?prevSum:"N/A") ##Printing prev and prevSum(if its NULL then print N/A).
    
'

【讨论】:

以上是关于来自 shell 的 GROUP BY/SUM的主要内容,如果未能解决你的问题,请参考以下文章

shell之找出100内被3整除的数

linux shell:提取正则表达式捕获组(catch group)匹配的字符串

来自 GROUP_BY 的两个 LEFT JOIN 的 GROUP_CONCAT 的奇怪重复行为

如何在 Java 中获取 Group.Captures(来自 C# 中的 RegEx)的行为?

来自变量的 XSLT for-each-group 不起作用

来自三个表的 JOIN 和 GROUP_CONCAT 的意外结果