awk '!a[$0]++' 是如何工作的?

awk '!a[$0]++' 是如何工作的?

这一行从文本输入中删除重复行,而无需预先排序。

例如:

$ cat >f
q
w
e
w
r
$ awk '!a[$0]++' <f
q
w
e
r
$ 

我在互联网上找到的原始代码如下:

awk '!_[$0]++'

这让我更加困惑,因为我认为它_在 awk 中具有特殊含义,就像在 Perl 中一样,但结果证明它只是一个数组的名称。

现在,我明白了这句话背后的逻辑: 每个输入行都用作散列数组中的键,因此,完成后,散列包含按到达顺序排列的唯一行。

我想了解的是 awk 到底如何解释这个符号。例如,感叹号 ( !) 的含义以及此代码片段的其他元素。

它是如何工作的?

答案1

这是一个“直观”的答案,有关 awk 机制的更深入解释,请参阅@Cuonglm 的

在这种情况下!a[$0]++,后置增量++可以暂时搁置,它不会改变表达式的值。所以,只看!a[$0].这里:

a[$0]

使用当前行$0作为数组的键a,并获取存储在那里的值。如果之前从未引用过此特定键,a[$0]则计算结果为空字符串。

!a[$0]

否定!之前的值。如果它为空或零(假),我们现在得到一个真实的结果。如果它非零(真),我们就会得到错误的结果。如果整个表达式的计算结果为 true,这意味着a[$0]未设置为开始,则整行将作为默认操作打印。

此外,无论旧值如何,后自增运算符都会加一a[$0],因此下次访问数组中的相同值时,它将是正数,整个条件将失败。

答案2

下面是处理过程:

  • a[$0]:查看$0关联数组中key 的值a。如果不存在,则自动使用空字符串创建它。

  • a[$0]++:增加 的值a[$0],返回旧值作为表达式的值。++运算符返回一个数值,因此如果a[$0]一开始为空,则0返回 并a[$0]增加到1

  • !a[$0]++: 否定表达式的值。如果a[$0]++返回0(假值),则整个表达式计算结果为 true,并执行awk默认操作print $0。否则,如果整个表达式的计算结果为 false,则不采取进一步的操作。

参考:

有了gawk,我们可以使用dgawk(或awk --debug更新版本)调试gawk脚本。首先,创建一个gawk脚本,名为test.awk

BEGIN {                                                                         
    a = 0;                                                                      
    !a++;                                                                       
}

然后运行:

dgawk -f test.awk

或者:

gawk --debug -f test.awk

在调试器控制台中:

$ dgawk -f test.awk
dgawk> trace on
dgawk> watch a
Watchpoint 1: a
dgawk> run
Starting program: 
[     1:0x7fe59154cfe0] Op_rule             : [in_rule = BEGIN] [source_file = test.awk]
[     2:0x7fe59154bf80] Op_push_i           : 0 [PERM|NUMCUR|NUMBER]
[     2:0x7fe59154bf20] Op_store_var        : a [do_reference = FALSE]
[     3:0x7fe59154bf60] Op_push_lhs         : a [do_reference = TRUE]
Stopping in BEGIN ...
Watchpoint 1: a
  Old value: untyped variable
  New value: 0
main() at `test.awk':3
3           !a++;
dgawk> step
[     3:0x7fe59154bfc0] Op_postincrement    : 
[     3:0x7fe59154bf40] Op_not              : 
Watchpoint 1: a
  Old value: 0
  New value: 1
main() at `test.awk':3
3           !a++;
dgawk>

可以看到,Op_postincrement之前已经被执行过Op_not

您还可以使用siorstepi代替sstep来看得更清楚:

dgawk> si
[     3:0x7ff061ac1fc0] Op_postincrement    : 
3           !a++;
dgawk> si
[     3:0x7ff061ac1f40] Op_not              : 
Watchpoint 1: a
  Old value: 0
  New value: 1
main() at `test.awk':3
3           !a++;

答案3

啊,无处不在但又不祥的 awk 重复删除器

awk '!a[$0]++'

这个可爱的宝贝是 awk 的强大功能和简洁性的宠儿。 awk oneliners 的顶峰。简短但有力且神秘。在保持顺序的同时删除重复项。未实现的壮举uniqsort -u仅删除相邻的重复项或必须破坏顺序才能删除重复项。

我试图解释这个 awk oneliner 是如何工作的。我努力解释事情,以便那些不懂 awk 的人仍然可以跟上。我希望我能够这样做。

首先介绍一些背景知识:awk 是一种编程语言。此命令awk '!a[$0]++'在 awk 代码上调用 awk 解释器/编译器!a[$0]++。类似于python -c 'print("foo")'node -e 'console.log("foo")'。awk 代码通常是单行的,因为 awk 专门设计用于简洁的文本过滤。

现在一些伪代码。这个衬垫的作用基本上如下:

for every line of input
  if i have not seen this line before then
    print line
  take note that i have now seen this line

我希望您能看到如何在保持顺序的同时删除重复项。

但是循环、if、打印以及存储和检索字符串的机制如何适合 8 个字符的 awk 代码呢?答案是隐含的。

循环、if 和 print 是隐式的。

为了解释一下,让我们再次检查一些伪代码:

for every line of input
  if line matches condition then
    execute code block

这是一个典型的过滤器,您可能已经在任何语言的代码中以某种形式编写了很多该过滤器。 awk 语言的设计使得编写此类过滤器的时间非常短。

awk 为我们做了循环,所以我们只需要在循环内编写代码。 awk 的语法进一步省略了 if 的样板,我们只需要编写条件和代码块:

condition { code block }

在 awk 中,这称为“规则”。

我们可以省略条件或代码块(显然我们不能同时省略两者),awk 会用一些隐式填充缺失的部分。

如果我们省略条件

{ code block }

那么它将是隐含的 true

true { code block }

这意味着代码块将针对每一行执行

如果我们省略代码块

condition

那么它将隐式打印当前行

condition { print current line }

让我们再看一下原来的 awk 代码

!a[$0]++

它不位于花括号内,因此它是规则的条件部分。

让我们写出隐式循环以及 if 和 print

for every line of input
  if !a[$0]++ then
    print line

与我们原来的伪代码进行比较

for every line of input                      # implicit by awk
  if i have not seen this line before then   # at least we know the conditional part
    print line                               # implicit by awk
  take note that i have now seen this line   # ???

我们了解循环、if 和打印。但它是如何工作的,以便仅在重复行时计算结果为 false?它如何记录已经看到的线条?

让我们拆开这个野兽:

!a[$0]++

如果您了解一些 c 或 java,您应该已经知道一些符号。语义相同或至少相似。

感叹号( !) 是否定词。它将表达式计算为布尔值,无论结果如何,它都会被否定。如果表达式计算结果为 true,则最终结果为 false,反之亦然。

a[..]是一个数组。关联数组。其他语言将其命名为地图或字典。在 awk 中,所有数组都是关联数组。没有a特殊意义。它只是数组的名称。也可以是xeliminatetheduplicate

$0是输入的当前行。这是 awk 特定的变量。

plus plus ( ++) 是后置增量运算符。这个运算符有点棘手,因为它做了两件事:变量中的值递增。但它也“返回”原始的、不增加的值以供进一步处理。

   !        a[         $0       ]        ++
negator   array   current line      post increment

他们如何一起工作?

大致按照这个顺序:

  1. $0是当前行
  2. a[$0]是数组中当前行的值
  3. 后增量 ( ++) 从 获取值a[$0];递增并将其存储回a[$0];然后将原始值“返回”到行中的下一个运算符:求反器。
  4. 取反器 ( !) 从 中获取一个值,++该值是来自 的原始值a[$0];它被评估为布尔值,然后取反,然后传递给隐式 if。
  5. if 然后决定是否打印该行。

所以这意味着该行是否被打印,或者在这个 awk 程序的上下文中:该行是否重复,最终由 中的值决定a[$0]

++通过扩展:当将递增的值存储回时,必须发生记录该行是否已被看到的机制a[$0]

让我们再看一下我们的伪代码

for every line of input
  if i have not seen this line before then   # decided based on value in a[$0]
    print line
  take note that i have now seen this line   # happens by increment from ++

你们中的一些人可能已经知道这是如何进行的,但我们已经走了这么远,让我们采取最后几步并采取行动++

我们从嵌入隐式的 awk 代码开始

for each line as $0
  if !a[$0]++ then
    print $0

让我们引入变量以留出一些工作空间

for each line as $0
  tmp = a[$0]++
  if !tmp then
    print $0

现在我们把它拆开++

请记住,该运算符执行两件事:增加变量中的值并返回原始值以供进一步处理。所以++变成两行:

for each line as $0
  tmp = a[$0]       # get original value
  a[$0] = tmp + 1   # increment value in variable
  if !tmp then
    print $0

或者换句话说

for each line as $0
  tmp = a[$0]       # query if have seen this line
  a[$0] = tmp + 1   # take note that has seen this line
  if !tmp then
    print $0

与我们的第一个伪代码进行比较

for every line of input:
  if i have not seen this line before:
    print line
  take note that i have now seen this line

因此,我们有它。我们有循环、if、打印、查询和笔记。只是顺序与伪代码不同。

压缩为8个字符

!a[$0]++

可能是因为 awks 隐式循环、隐式 if、隐式打印,并且因为它++同时执行查询和记录。

仍然是一个问题。a[$0]第一行的值是多少?或者任何以前从未见过的线路?答案又是隐含的。

在 awk 中,第一次使用的任何变量都会被隐式声明并初始化为空字符串。除了数组。数组被声明并初始化为空数组。

++式转换为数字。空字符串转换为零。其他字符串将通过某种尽力而为的算法转换为数字。如果字符串未被识别为数字,它会再次转换为零。

隐式转换为布尔值!。数字零和空字符串转换为 false。其他任何内容都会转换为 true。

这意味着当第一次看到一行时,它a[$0]被设置为空字符串。空字符串被转换为零++(也增加到 1 并存储回a[$0])。零通过 转换为假!。结果为!true,因此该行被打印。

现在的值a[$0]是数字 1。

如果第二次看到一行,则a[$0]数字 1 会转换为 true,而结果!为 false,因此不会打印。

同一行的任何进一步相遇都会增加数量。由于除零之外的所有数字均为 true,因此结果!将始终为 false,因此该行永远不会再次打印。

这就是删除重复项的方法。

长话短说:它计算一条线被看到的频率。如果为零则打印。如果有任何其他数字则不打印。由于许多隐含的内容,它可能很短。


奖励:单行代码的一些变体以及对其功能的超简短解释。

$0将(整行)替换为$2(第二列)将删除重复项,但仅基于第二列

$ cat input 
x y z
p q r
a y b

$ awk '!a[$2]++' input 
x y z
p q r

!将(否定符)替换为==1(等于一),它将打印重复的第一行

$ cat input 
a
b
c
c
b
b

$ awk 'a[$0]++==1' input 
c
b

替换为>0(大于零)并添加{print NR":"$0}将打印所有重复的行以及行号。NR是一个特殊的 awk 变量,包含行号(awk 行话中的记录号)。

$ awk 'a[$0]++>0 {print NR":"$0}' input 
4:c
5:b
6:b

我希望这些例子有助于进一步掌握上面解释的概念。

答案4

只是想补充一点expr++, 和++expr只是 的简写expr=expr+1。但

$ awk '!a[$0]++' f # or 
$ awk '!(a[$0]++)' f

将打印所有唯一值,因为将在添加之前expr++求值,而expr

$ awk '!(++a[$0])' f

将只打印任何内容,因为++expr将计算为expr+1,在这种情况下始终返回非零值,而求反将始终返回零值。

相关内容