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,在這種情況下始終傳回非零值,而求反將始終傳回零值。

相關內容