这一行从文本输入中删除重复行,而无需预先排序。
例如:
$ 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
。
您还可以使用si
orstepi
代替s
或step
来看得更清楚:
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 的顶峰。简短但有力且神秘。在保持顺序的同时删除重复项。未实现的壮举uniq
或sort -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
特殊意义。它只是数组的名称。也可以是x
或eliminatetheduplicate
。
$0
是输入的当前行。这是 awk 特定的变量。
plus plus ( ++
) 是后置增量运算符。这个运算符有点棘手,因为它做了两件事:变量中的值递增。但它也“返回”原始的、不增加的值以供进一步处理。
! a[ $0 ] ++
negator array current line post increment
他们如何一起工作?
大致按照这个顺序:
$0
是当前行a[$0]
是数组中当前行的值- 后增量 (
++
) 从 获取值a[$0]
;递增并将其存储回a[$0]
;然后将原始值“返回”到行中的下一个运算符:求反器。 - 取反器 (
!
) 从 中获取一个值,++
该值是来自 的原始值a[$0]
;它被评估为布尔值,然后取反,然后传递给隐式 if。 - 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
,在这种情况下始终返回非零值,而求反将始终返回零值。