![awk '!a[$0]++' 是如何運作的?](https://rvso.com/image/57077/awk%20'!a%5B%240%5D%2B%2B'%20%E6%98%AF%E5%A6%82%E4%BD%95%E9%81%8B%E4%BD%9C%E7%9A%84%EF%BC%9F.png)
這一行從文字輸入中刪除重複行,而無需預先排序。
例如:
$ 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
,在這種情況下始終傳回非零值,而求反將始終傳回零值。