使用 sed (或 awk)汇总特定字段中包含相同值的连续行?

使用 sed (或 awk)汇总特定字段中包含相同值的连续行?

到目前为止我已经能够四处走动sed 的更高级的功能,例如跨多行的向前/向后查看,但我想了解如何实现以下任务sed因为我感觉我这样做的方法例如在Python不是必需的,也可以在过滤器管道内完成指令

传入数据的剥离示例如下所示:

1b41cf70 0
1cb8dd19 1
620f0b67 2
620f0b67 3
f35d35fe 4
3a6fb62a 5
620f0b67 6
620f0b67 7
620f0b67 8
b958a7ea 9
f35d35fe 10
f35d35fe 11
620f0b67 12

第一列的宽度始终相同(包含缩短的哈希值),第二列的内容是完全排序的、数字的且没有间隙(因此除了在较长的列表上提供方向之外可能没有必要)。

所需的输出将是这样的(将最后一次连续出现的索引放入附加列中):

1b41cf70 0
1cb8dd19 1
620f0b67 2 3
f35d35fe 4
3a6fb62a 5
620f0b67 6 8
b958a7ea 9
f35d35fe 10 11
620f0b67 12

或者甚至更好地使用重复值的聚合数量(数学表达式(添加)似乎更容易完成awk但我的技能更差,所以这只是为了说明其他理想的结果):

1b41cf70 0
1cb8dd19 1
620f0b67 2 +1
f35d35fe 4
3a6fb62a 5
620f0b67 6 +2
b958a7ea 9
f35d35fe 10 +1
620f0b67 12

我试图追踪在 SO 空间中发现的几个相似但不同的问题,但无法将我的头脑集中在可能更简单的部分上,可能会导致一个解决方案,比如为什么sed '$!N;/^\([^\ ]\+\)\ [0-9]\+\n\1\ /{P;d}' sampledata会用索引 3,7,11 而不是 8 来切割行。

我的系统安装了 GNU sed 版本 4.8 和 awk 版本 5.1.0,我很想了解如何使用其中之一来完成此任务。不,这不是家庭作业,而是冗长的哈希列表,其中有大量冗余,需要压缩和比较。 ;)

答案1

完全忽略原来的第二列,我们可以用来uniq -c计算字符串在连续行中重复出现的次数。

从 中获取两个字段的输出uniq -c,只要字符串重复出现多次(在表格中+x,其中x是该字段出现的次数减一),我们就可以创建第三个字段。然后我们重新排列前两个字段并打印。

cut -d ' ' -f 1 file |
uniq -c |
awk '$1 > 1 { $3 = "+" $1 - 1 } { nr += $1; $1 = $2; $2 = nr - 1 - $3; print }'

nr变量表示原始文件中的行号。

给出问题中数据的输出:

1b41cf70 0
1cb8dd19 1
620f0b67 2 +1
f35d35fe 4
3a6fb62a 5
620f0b67 6 +2
b958a7ea 9
f35d35fe 10 +1
620f0b67 12

答案2

使用awk

awk 'function prnt() { print buf, preV; preK=$1; preV=""; buf=$0 }
preK!=$1             { prnt(); next } { preV=$2 }
END                  { prnt() }' infile

输出:

1b41cf70 0
1cb8dd19 1
620f0b67 2 3
f35d35fe 4
3a6fb62a 5
620f0b67 6 8
b958a7ea 9
f35d35fe 10 11
620f0b67 12

awk 'function prnt() { print buf, (c?"+"c:""); preK=$1; c=0; buf=$0 }
preK!=$1             { prnt(); next } { c++ }
END                  { prnt() }' infile

输出:

1b41cf70 0
1cb8dd19 1
620f0b67 2 +1
f35d35fe 4
3a6fb62a 5
620f0b67 6 +2
b958a7ea 9
f35d35fe 10 +1
620f0b67 12

答案3

你要求的sed。这里有 2 个版本接近您自己的尝试,但使用 POSIX埃雷 扩展正则表达式。两者都在模式空间中最多保留 2 行。

sed -E '
    :Q
    $!N
    /^([^ ]+) ([0-9]+)( [0-9]+)?\n\1 ([0-9]+)$/{
        s//\1 \2 \4/
        bQ
    }
    P
    D
' -- file

在哪里:

  • 除非在最后一行 ( $!) 附加换行符并将下一行附加到当前行 ( N)
  • 匹配表达式/…/将字段 1 和 2 捕获为\1and \2,可能的最后一个索引为\3,最后下一行的索引为\4
  • 如果字段 1 在下一行重复,则整个模式空间将替换为字段 1(哈希)、字段 2(第一个索引)和最后一个索引,并分支到脚本开头 - 命令中的空正则表达式s将重新应用最后使用的正则表达式(在/…/
  • 否则打印并删除第一行 ( P;D;) 并恢复循环

输出:

1b41cf70 0
1cb8dd19 1
620f0b67 2 3
f35d35fe 4
3a6fb62a 5
620f0b67 6 8
b958a7ea 9
f35d35fe 10 11
620f0b67 12

如果相反:

/^([^ ]+) ([0-9]+)( ([+]+))?\n\1 [0-9]+$/{
    s//\1 \2 \4+/

输出变为:

1b41cf70 0
1cb8dd19 1
620f0b67 2 +
f35d35fe 4
3a6fb62a 5
620f0b67 6 ++
b958a7ea 9
f35d35fe 10 +
620f0b67 12

sed不热衷于计数(但可以做到)。


sed最后,对使用 POSIX 的脚本进行一些评论布雷s

  • 不要[]对 s 内的字符进行转义,转义字符除外。, ], 以及可能的转义字符-
  • BRE 中的+不是量词而是普通的加号
  • 不需要转义空格字符
  • }为了便于移植,在结束编辑命令列表之前使用分号
  • d命令删除整个模式空间,而不仅仅是第一个换行符

答案4

只是一个快速混淆的多行查找替换解决方案(这次是在 Perl 中)

perl -0pe 's/(\w+) (\d+)(\n\1 (\d+))+/$1 $2 $4/g' file

相应的 (gnu)sed 版本可能是...

sed -rz 's/(\w+) ([0-9]+)(\n\1 ([0-9]+))+/\1 \2 \4/g' file

对于“+”输出,我们必须做一些额外的计算:

perl -0pe 's/(\w+) (\d+)(\v\1 (\d+))+/"$1 $2 +" . ($4-$2)/ge' file

相关内容