awk:按第一列排序,然后按第二列排序;输出唯一的第一列一次,但输出所有第二列

awk:按第一列排序,然后按第二列排序;输出唯一的第一列一次,但输出所有第二列

我有一个两列文件,您可以按如下方式创建

cat > twocol << EOF
007 03
001 03
003 01
137 12
001 11
002 01
002 02
002 03
001 02
002 04
137 94
010 21
001 01
EOF

生成的文件twocol只包含数字行。


期望的结果

我想执行某种命令twocol并得到以下结果。 (我认为看到它比尝试重述我有点令人困惑的问题标题要好得多 - “按第一列排序,然后第二列排序;输出唯一的第一列一次,但输出所有第二列”。)

001 01
    02
    03
    11
002 01
    02
    03
    04
003 01
007 03
010 21
137 12
    94

这与 simplesort给我的不同,即不同于

001 01
001 02
001 03
001 11
002 01
002 02
002 03
002 04
003 01
007 03
010 21
137 12
137 94

我的工作

我想到的唯一解决方案是我想出的第一个解决方案(在我得到一个像样的awk脚本之前) - 它与上面粗体的所需结果相匹配,使用了几个实例awk,一堆bash,以及来自的一些帮助1

col_1_max_len=$(awk '
  BEGIN{maxl=0;}
  {curr=length($1);max1=max1>curr?max1:curr;}
  END{print max1}' \
 twocol);
len1=$col_1_max_len;
len2=$(awk '
  BEGIN{max2=0;}
  {curr=length($2);max2=max2>curr?max2:curr;}
  END{print max2}' \
 twocol);
current_col_1_val="nothing";

while read -r line; do {
  current_row="${line}";
  col_1_val=$(awk '{print $1}' <<< "${current_row}");
  col_2_val=$(awk '{print $2}' <<< "${current_row}");
  if [ ! "${col_1_val}" == "${current_col_1_val}" ]; then
    printf "%0"$len1"d %0"$len2"d\n"  "${col_1_val}"  "${col_2_val}";
  else
    printf "%"$len1"s %0"$len2"d\n"  " "  "${col_2_val}";
  fi;
}; done < <(sort twocol)

我觉得我应该能够使用一次传递awk,类似于以下答案:2,3,4,5, ...

如果没有额外的、笨重的、消耗内存的数组,我似乎无法将它拼凑在一起。这种格式也给我带来了一个问题——第一列和第二列中的数字可以有更多位数,而且最好看起来不错。

谁能告诉我如何通过一些好的方法得到这个结果 awk 代码 - 最好可以在终端中轻松使用? Perl 也欢迎回答。


哦,我的系统

$ uname -a && bash --version | head -1 && awk --version | head -1
CYGWIN_NT-10.0 MY-MACHINE 3.2.0(0.340/5/3) 2021-03-29 08:42 x86_64 Cygwin
GNU bash, version 4.4.12(3)-release (x86_64-unknown-cygwin)
GNU Awk 5.1.0, API: 3.0 (GNU MPFR 4.2.0-p9, GNU MP 6.2.1)

(我在 Fedora 和 Ubuntu 机器上得到了完全相同的行为。)



编辑

我想出了一个awk解决方案。看起来一切都很好,很短,但我仍然觉得有问题。

awk '{if (!vals[$1]++) print($0); else print("   ",$2);}' <(sort twocol)

我认为我在数组中使用了一堆内存vals- 截至目前,我的文件只有约 10k 行,但我希望将其扩大。我以格式进行硬编码,但我不喜欢它,因为我可以有不同长度的字符串。

如果我使用变量进行三遍awk并传递变量,我可以修复这个问题(格式)。

length1=$(awk '
  BEGIN{maxl=0;}
  {curr=length($1);max1=max1>curr?max1:curr;}
  END{print max1}' \
 twocol);

length2=$(awk '
  BEGIN{max2=0;}
  {curr=length($2);max2=max2>curr?max2:curr;}
  END{print max2}' \
 twocol);

awk -vlen1=$length1 -vlen2=$length2 '
{
  if (!vals[$1]++) 
    printf("%0*d %0*d\n",len1,$1,len2,$2); 
  else 
    printf("%*s %0*d\n",len1," ",len2,$2);
}' <(sort twocol)

结果与所需结果完全匹配(请参阅上面粗体部分),但我希望有一种方法可以通过一次awk.

谁能分享一些符合我提到的特征的东西?关于不同方法的时间性能和/或内存性能的任何评论也将受到赞赏。

我认为也可以进行排序awk;我想知道,尤其是它是否可以更有效率。编辑:这是可以完成的,如下面的 @steeldriver 和 @markp-fuso 所示。

答案1

原始 awk 解决方案已删除 - a更好的解决方案已发布


实际上,您可以对输入进行预排序,然后使用(任何)awk 对其进行格式化:

sort twocol | awk 'BEGIN{OFS="\t"} {print $1 == last ? "" : $1, $2; last = $1}'
001     01
        02
        03
        11
002     01
        02
        03
        04
003     01
007     03
010     21
137     12
        94

这会产生制表符分隔的输出 - 如果需要空格,请通过管道传输结果expand


或者,您可以使用匿名数组的 Perl 哈希来聚合第二列值,然后排序并打印:

perl -alne '
  push @{ $h{$F[0]} }, $F[1] 
  }{ 
  foreach $k (sort {$a <=> $b} keys %h) {
    @a = sort {$a <=> $b} @{ $h{$k} };
    print join "\n", map { ($_ == 0 ? $k : "") . "\t" . $a[$_] } 0..$#a;
  }
  ' twocol
001     01
        02
        03
        11
002     01
        02
        03
        04
003     01
007     03
010     21
137     12
        94

这些{$a <=> $b}可能不是必需的,因为您的零填充数据按字典顺序排序与按数字排序相同。


只是为了好玩,与磨坊主:

mlr -S --nidx --ofs tab put -q '
  @m[$1] = is_not_array(@m[$1]) ? [$2] : append(@m[$1],$2); 
  end { 
    @m = sort(apply(@m, func(k,v) { return {k: joinv(sort(v), "\n\t")}; }));
    emit @m, ""
  }
  ' twocol 
001     01
        02
        03
        11
002     01
        02
        03
        04
003     01
007     03
010     21
137     12
        94

答案2

一个awk想法:

awk '
BEGIN { OFS="\t"  }
      { a[$1][$2] }                               # we can sort on both indices to obtain the desired ordering
END   { PROCINFO["sorted_in"] = "@ind_num_asc"    # applies to all follow-on array references (ie, both indices of the a[] array)
        for (i in a) {
            firstcol = i
            for (j in a[i]) {
                print firstcol, j
                firstcol = ""
            }
         }
      }
' twocol

笔记:GNU awk 4.0+需要PROCINFO["sorted_in"]支持

这会生成:

001     01
        02
        03
        11
002     01
        02
        03
        04
003     01
007     03
010     21
137     12
        94

如果PROCINFO["sorted_in"]不可用,我们可以使用sort它来提供简化的awk脚本:

awk '
BEGIN { OFS="\t" }
      { if ($1 != prev1) {
           print $1,$2
           prev1 = $1
        }
        else
           print "",$2
      }
' < <(sort twocol)

这也生成:

001     01
        02
        03
        11
002     01
        02
        03
        04
003     01
007     03
010     21
137     12
        94

答案3

使用(以前称为 Perl_6)

~$ raku -ne 'BEGIN my %h;  %h.append: .split(/ \s+ /);  END put .key => .value.sort.join("\n\t") for %h.sort;'  file

#OR

~$ raku -ne 'BEGIN my %h;  %h.append: .words;  END put .key => .value.sort.join("\n\t") for %h.sort;'  file

这是用 Raku(Perl 编程语言家族的成员)编写的答案。简而言之,上面的代码有点awk像 - 并使用 Raku 的(如 Perl 的)-ne非自动打印命令行标志。

  • 哈希值%h在块中声明BEGIN
  • 该行位于.split一个或多个\s空格字符上。或者(第二个答案),Raku 的.words例程用于按空格进行分割。对于这两个答案,结果(两个)元素被理解为一个键值对,它被append编辑到哈希中。
  • END块中,%h哈希值(sort键上的 ed)单独输出put,每个值.key后面跟着每个.value已经存在的值sort.join("\n\t")。将换行到下一行的值移动\t到第二列中。

输入示例:

007 03
001 03
003 01
137 12
001 11
002 01
002 02
002 03
001 02
002 04
137 94
010 21
001 01

示例输出:

001 01
    02
    03
    11
002 01
    02
    03
    04
003 01
007 03
010 21
137 12
    94

请注意,有时查看 Raku 的默认值是有启发性的,因此这里是没有“列化”上面的输出的答案(即下面更简单的代码):

~$ raku -ne 'BEGIN my %h; %h.append: .words; END say .key => .value.sort for %h.sort;'  file
001 => (01 02 03 11)
002 => (01 02 03 04)
003 => (01)
007 => (03)
010 => (21)
137 => (12 94)

https://docs.raku.org/type/Hash
https://raku.org

相关内容