awk 小傳

awk 是一種專事文本解析與處理的解釋型編程語言,其解釋器與其同名注 1。awk 原始版本發佈於 1977 年,後於 1985 年發佈第一個加強版本。在這一時期,awk 羽翼漸豐,隨後成爲 Unix 系統的一個標準(POSIX 標準)組件。目前 Linux 系統配備的 awk 皆爲 gawk,隸屬 GNU 項目,伊始於 1986 年。python

注 1:awk 得名於它的三位開發者 Alfred V. Aho、Peter J. Weinberger 以及 Brian W. Kernighan 名字的首字母。

awk 的設計深受 Unix 系統中的文本檢索工具 grep 與文本編輯工具 sed 的啓發,其語法借鑑了 C 語言。正則表達式

模式-動做

「模式-動做」邏輯是 awk 語言的精華所在。awk 認爲自身所處理的文本文件裏依序存儲着一組記錄。awk 默認將文件中的每一行視爲一條記錄注 2,經過模式檢索特定記錄,然後經過動做修改記錄的內容。模式由邏輯表達式或正則表達式構成,而動做則由一組用於分析或處理文本的語句構成。在 awk 解釋器看來,模式和動做造成的總體即程序,文件裏的每條記錄則爲程序的輸入數據,而動做所產生的結果則爲程序的輸出數據。數據庫

注 2:下文在講述記錄分割符變量 RS 時,再給出讓 awk 將多行文本視爲一條記錄的方法。

例如,對於任意一份文本文件 foo.txt,若僅輸出其第三行,可經過 awk 程序編程

NR == 3 { print $0 }

來實現,其中 NR == 3 爲模式,而 print $0 爲動做。NR$0 皆爲 awk 解釋器內部維護的變量,前者用於保存 awk 解釋器目前所讀入的記錄的序號,後者用於保存記錄的內容。數組

將上述 awk 程序以及 foo.txt 做爲 awk 解釋器的輸入數據,即在終端中執行編程語言

$ awk 'NR == 3 { print $0 }' foo.txt

則 awk 解釋器的輸出結果即爲 foo.txt 的第三行——awk 解釋器默認將其視爲第三條記錄。爲了防止 Shell 誤解 awk 程序中的一些成分,必須將後者用單引號拘禁。函數

上述命令詮釋了 awk 解釋器、awk 程序以及所處理的文本文件這三者之間的聯繫。在 awk 的實際應用中,咱們須要對所處理的文本文件中的記錄有足夠的瞭解,剩下的任務則是編寫 awk 程序,即工具

模式 { 動做 }

awk 解釋器會順序讀取所處理的文本文件裏的每一條記錄,並驗證它是否與模式相匹配。凡是與模式相匹配的記錄,便會受模式以後的動做的操控。命令行

模式-動做可疊加起來做用於文件的每一條記錄。例如,設計

awk 'NR == 3 { print $0 }; NR == 4 { print $0 }' foo.txt

能夠輸出 foo.txt 的第 三、4 行。

不妨將 awk 程序裏的動做理解爲電路,將模式理解爲開關。記錄與模式匹配時,至關於觸動了開關,使得電路得以導通。多條模式-動做,至關於有多個候選的開關及電路。

模式能夠爲空,例如

$ awk '{ print $0 }' foo.txt

此時,awk 會將動做做用於每一條記錄。相似地,動做也能夠爲空,例如

$ awk 'NR == 3' foo.txt

此時,awk 會輸出與模式相匹配的每一條記錄,這一動做正是 { print $0 }

模式-動做的邏輯本質上是全部編程語言中的條件分支結構的泛化。Erlang、Racket、OCaml、Haskell、Swift 等語言提供了令其愛好者們引覺得傲的模式匹配的語法所反映的也是這一邏輯。如今,知道了早在 1977 年 awk 便已經極爲天然地創建了這一邏輯,應當由衷而嘆,太陽底下果真沒什麼新鮮事!

腳本

爲了讓 awk 解釋器可以在讀取文件以前以及全部記錄處理殆盡以後也可以有所動做,awk 語言提供了 BEGINEND 這兩個特殊的模式。所以較爲完整的 awk 程序,其結構一般爲

BEGIN { 讀取文件以前的動做 }
模式 1 { 動做 1 }
模式 2 { 動做 2 }
... ... ...
模式 n { 動做 n }
END { 全部記錄處理殆盡以後的動做 }

如此便難以在終端裏以命令參數的形式將程序交於 awk 解釋器。再者,對於複雜的 awk 程序,若欲重複使用,命令參數的形式也多極爲不便。爲此,awk 解釋器支持以腳本的形式載入程序,亦便可將 awk 程序保存爲一份文件,然後讓 awk 讀取該文件以得到程序。此種文件即 awk 腳本。

awk 解釋器可經過 -f 選項載入腳本中的程序。例如,製做一份簡單的 awk 腳本 hello-world.awk,

$ cat << 'EOF' > hello-world.awk
BEGIN {
        print "Hello"
}

NR == 5 {
        print $0
}

NR == 6 {
        print $0
}

END {
        print "world!"
}
EOF

而後隨意創建一份文本文件 foo.txt,

$ cat << EOF > foo.txt
以指喻指之非指
不若以非指喻指之非指也
以馬喻馬之非馬
不若以非馬喻馬之非馬也
天地一指也
萬物一馬也
EOF

若執行

$ awk -f hello-world.awk foo.txt

則結果爲

Hello
天地一指也
萬物一馬也
world!

腳本 hello-world.awk 的內容可簡化爲

BEGIN {
        print "Hello"
}

NR == 5 || NR == 6 {
        print $0
}

END {
        print "world!"
}

還可進一步簡化爲

BEGIN {
        print "Hello"
}

NR == 5 || NR == 6

END {
        print "world!"
}

|| 是邏輯運算「或」。awk 中基本的邏輯運算符號皆與 C語言同。

在 awk 語言中,; 與換行等價,所以上述內容亦可寫爲

BEGIN { print "Hello" }; NR == 5 || NR == 6; END { print "world!"}

流程控制語句

awk 提供了 if ... else if ... else 條件分支以及 forwhiledo ... while 等循環結構的語法,用法幾近於 C 語言,無需贅述。

正則表達式

若使用正則表達式做爲模式,則記錄與模式的匹配所表示的邏輯是前者包含着可與後者匹配的文本。

例如,若只輸出上一節的 foo.txt 文件中含有「馬」的記錄,能夠用 做爲模式,

$ awk '/馬/' foo.txt
以馬喻馬之非馬
不若以非馬喻馬之非馬也
萬物一馬也

awk 解釋器輸出的是 foo.txt 文件中含有可與正則表達式 匹配的文本的記錄。在 awk 語言中,正則表達式的兩側以 / 爲界。

下面給出幾個略微複雜一點的正則表達式做爲模式的例子。只輸出 foo.txt 中以 不若 做爲開頭的記錄,

$ awk '/^不若/' foo.txt
不若以非指喻指之非指也
不若以非馬喻馬之非馬也

只輸出 foo.txt 中以 做爲結尾的記錄,

$ awk '/也$/' foo.txt
不若以非指喻指之非指也
不若以非馬喻馬之非馬也
天地一指也
萬物一馬也

只輸出 foo.txt 中含有至少兩個 字的記錄,

$ awk '/馬.*馬/' foo.txt
以馬喻馬之非馬
不若以非馬喻馬之非馬也

awk 的正則表達式取自 egrep。egrep 爲 grep 的擴展版本,支持的正則表達式較 grep 更爲豐富。所以若熟悉 awk 的正則表達式,則自會熟悉 grep/egrep 的用法,反之亦然。

對於正則表達式與記錄的匹配,awk 提供了邏輯運算符 ~!~,前者相似 ==,表示匹配,然後者相似 !=,表示不匹配。所以

$ awk '/馬.*馬/' foo.txt

可寫爲

$ awk '$0 ~ /馬.*馬/' foo.txt

還能夠寫爲

$ awk '{ if ($0 ~ /馬.*馬/) print $0 }' foo.txt

變量

awk 的變量可無需初始化即可使用。例如,

$ awk 'NR == 1 { print a; print a + 1 }' foo.txt

1

即輸出了空行和內容爲 1 的行。之因此會輸出空行,是由於變量 a 未經初始化便在語句「print a」中使用,awk 解釋器默認其值爲空文本,即 "",從而致使 「print a」輸出空行。可是在「print a + 1」中,awk 解釋器發現 a 出現於算術表達式中,所以便將其值由空字串轉化爲數字 0,從而致使「print a + 1」輸出內容爲 1 的行。

事實上,awk 的變量只有兩種類型,文本和數字。awk 解釋器會根據變量是否出現於算術表達式之中而對其值爲文本仍是數字進行推斷。

在 awk 程序中,全部的變量皆爲全局變量,除非它以函數的參數形式出現。

函數

awk 的函數,其通常形式爲

function 函數名(參數 1, 參數 2, ..., 參數 n) {
        函數體
}

函數的調用與 C 同,但函數名與參數列表的左括號之間不能存在空格。

在函數中,除了做爲參數的變量,其餘全部變量皆能爲函數外部可見。例如,

$ cat << 'EOF' > func-test.awk
NR == 1 {
        t = mul(2, 3)
        print "t = " t "; z = " z "; x = " x
}

function mul(x, y) {
        z = x * y
        return z
}
EOF
$ awk -f func-test.awk foo.txt
t = 6; z = 6; x =

在與模式 NR == 1 相應的動做裏,雖未對變量 z 進行賦值,但程序的輸出結果卻代表其值爲 6,這是由於函數 mul 中的變量 z 在函數的外部可見。不過,在動做裏調用 mul 時,將 2 賦予函數的參數 x,但動做輸出的 x,其值爲空文本。所以,若在 awk 程序中對變量的做用域進行限定,惟一的辦法是讓變量以函數參數的形式出現。

awk 的變量頗相似 Bash,但 Bash 在函數內部可經過關鍵字 local 將變量的做用域限定在函數以內。不過,awk 的做者在函數的寫法上給出了一個建議:可將做用域限定於函數內部的變量置於參數列表尾部,並經過一組空格,使之與函數的參數有所區分。例如,

function mul(x, y,    z) {
        z = x * y
        return z
}

awk 默認將變量做爲全局變量的作法,使得編寫一個略微複雜一些的程序的過程像是在編筐或織布,全局變量像是緯線,操做變量的語句則像是經線。

數組

awk 提供了數組類型。數組元素能夠異構,但並不是連續存儲於一段內存空間。例如,

a[0] = "abc"; a[1] = 7; a[2] = "馬";  a[33] = "三十三"

這個數組,雖然含有下標爲 33 的元素,可是並不是由 34 個元素構成,而是由 4 個元素構成,並且這 4 個元素在數組中的排列也未必是按照下標的順序。awk 並未對數組元素的排列給出確切的定義,這主要依賴於 awk 解釋器的具體實現。

數組元素在排列上的不肯定,意味着 awk 的數組僅支持順序訪問,但不支持隨機訪問。訪問數組中的每一個值,可採用 for (下標變量 in 數組) { ... } 語法,例如,

for (i in a) { print a[i] }

若以

for (i = 0; i < n; i++) { print a[i] }

訪問數組元素,前提是要保證數組元素的下標的確從 0n

實際上,awk 數組的下標並不是數字,而是文本。例如,a[3]a["3"] 皆能訪問下標爲 3 的元素。Bash 的數組亦如此。

輸入與輸出

命令

$ awk '程序' 文本文件

$ awk -f 腳本 文本文件

是 awk 程序運行的通常方式。程序所需的外部數據可經由文本文件以記錄的形式傳入。

若不向 awk 解釋器提供文本文件,那麼 awk 解釋器便會將標準輸入(stdin)做爲程序所需的外部數據的來源。這意味着能夠經過管道向 awk 程序傳遞記錄。例如,如下程序可去除文本 " 白馬非馬 " 首尾的空白,

$ echo "    白馬非馬    " | awk '{ match($0, / *([^ ]+) */, a); print a[1] }'

若利用 awk 的默認以空格做爲列分隔符而且去除列內前導空白字符的特性,可將上述 awk 程序簡化爲

$ echo "    白馬非馬    " | awk '{ print $1 }'

awk 解釋器對於每條記錄,默認以空白字符做爲分割符,將記錄內容斬爲多段,每一段稱爲域;awk 會將域的數量存於內置變量 NF。域的內容依次存於 awk 解釋器的內置變量 $1$2、...、$n,並將各段內容的前導空白字符(空格或製表符)消除。$0 存儲未分割的整條記錄。

對記錄進行分割,這一特性使得 awk 程序在處理相似矩陣形式的文本表現的簡短精悍,常常能以簡短的一行程序完成其餘編程語言動輒須要數十行代碼方能完成的任務。例如,假設文件 emp.txt 內容爲

張三  4.00  0
李四  3.75  0
王五  4.00  10
鄭六  5.00  20
趙七  5.50  22
孫八  4.25  18

記錄了一組僱員的姓名、時薪以及工做時長。如今要製做一份薪水報表,即統計哪些人蔘與了工做,應發多少錢。若採用 awk 完成該任務,只需

$ awk '$3 > 0 { print $1 "\t" $2 * $3 }' emp.txt

結果可得

王五    40
鄭六    100
趙七    121
孫八    76.5

若不只給出每一個人的薪水狀況,還要給出總的支出金額,只需

$ awk '$3 > 0 { x = $2 * $3; s += x; print $1 "\t" x }; END {print "\n總額\t" s}' emp.txt
王五    40
鄭六    100
趙七    121
孫八    76.5

總額    337.5

awk 解釋器的內置變量 RSFS 分別用於設定記錄分割符和域分割符,亦即經過設定此兩者,可以讓 awk 解釋器以記錄和域的形式理解輸入的數據。顯然,應當在 awk 解釋器讀取文件設置 RSFS,所以,它們的設定應當在 BEGIN 模式所對應的動做裏進行,例如,

BEGIN { RS = FS = "" }

此時,RSFS 皆爲空文本,但兩者含義不一樣,前者表示一個或多個空行做爲記錄分割符,然後者則以空文本做爲域分割符。

對於 Markdown 格式的中文文檔,各個段落以一個或多個空行隔開,而各個漢字之間則以空文本隔開。若將 RSFS 設爲空文本,那麼即可以很容易寫出一個統計文檔中漢字頻率的 awk 程序。例如,統計一份文檔中出現最多的十個漢字,只需

BEGIN { RS = FS = "" }
{ 
        # 移除標點符號、數字、英文字母以及空白字符
        gsub(/[.,:;!?(){}'",。:;!?()《》「」 ‘’a-zA-Z0-9  ]/, "")
        for (i = 1; i <= NF; i++) count[$i]++ 
}
END { for (i in count) print i, count[i] | "sort -rn -k 2 | head" }

我用這個程序統計的《莊子·逍遙遊》中出現最多的十個漢字及出現次數爲

之 70
而 55
也 51
不 42
其 35
者 26
爲 25
無 24
大 24
有 22

上述的 awk 程序,在實現漢字出現次數的排序以及排序結果的部分輸出時,藉助了 awk 的 print 函數與 sort 和 head 命令的管道銜接。此舉彷佛有些勝之不武,可是也沒什麼不妥,反而顯現了 awk 在文本輸出方面與 Linux(或其餘類 Unix 系統)系統命令行環境的親和性。

awk 解釋器經過 RSFS 理解做爲輸入的文件。對於 awk 程序的輸出,則存在這相應的變量 ORSOFS,awk 解釋器經過它們理解如何將程序所得結果輸出。

結語

一些繁瑣的文本分析方面的問題,一般可以以簡短的 awk 程序來解決。有些人由此看到了 awk 之美,有些人看到的則是 awk 之醜。所以,awk 的一行程序,吸引了許多人,也嚇走了許多人。

在我看來,awk 不美,不醜,也不老。像大多數依然健在的古老的工具那樣,只作一些恰如其分的事,這反而使之難以被取代。

egrep 和 sed 也只作恰如其分之事,前者專事文件檢索,後者專事文本編輯。此兩者所具備的功能,awk 皆能實現,但 awk 的出現並未取代它們。由於有些任務,用 grep 和 sed 能夠更快捷地完成,而用 awk 就有些繁瑣。例如,若獲取 foo.txt 中至少含有兩個 字的行及其序號,可完成這一任務的 awk 命令爲

$ awk '/馬.*馬/ { print NR ":" $0 }' foo.txt

如果用 egrep,只需

$ egrep -n '馬.*馬' foo.txt

若刪除 foo.txt 文件中以 天地萬物 開始的行,可完成這一任務的 awk 命令爲

$ awk '$0 !~ /^天地/ || '$0 !~ /^萬物/' foo.txt > new-foo.txt
$ mv new-foo.txt foo.txt

如果用 sed,只需

$ sed -i '/^天地/d; /^萬物/d' foo.txt

功能更強大的事物的出現,並不意味着功能孱弱的事物失去價值,反而可能更爲彰顯後者的價值。因此,不要隨便地就對別人說,「我已習得 python,還有必要再學 awk 嗎?」假若 awk 不能並且無心於取代 egrep 和 sed,那麼 python 當如何取代 awk 呢?

本文所展示的 awk 功能約有十之六七,旨在揭示 awk 的主要功用。掌握這些功能,足以勝任常規的文本處理任務。本文中出現的文字註釋形式,即是藉助 awk 生成,程序爲

BEGIN {
    note_pat = "\\\\note{([^}]+)}"
    i = 1
}
{
    # 註解 -> 上標
    current_note = i
    while (x = match($0, note_pat, s)) {
        notes[i] = s[1]
        sub(note_pat, "**<sup>注 " i++ "</sup>**", $0)
    }
    # 輸出處理後的段落及註解列表
    print $0
    if (i > current_note) {  # 段後增長註解
        print ""
        it = current_note
        while (it < i) {
            print "> 注 " it ":" notes[it]
            it++
        }
    }
}

有關 awk 的更爲詳盡的知識可從 awk 的三位開發者撰寫的《The AWK Programming Language》一書中得到。這本書雖然寫於 1988 年,但其內容依然適於如今的 awk 。此外,這本書在介紹 AWK 的編程示例中,言簡意賅地介紹了文本處理、數據庫、編譯原理、排序以及圖的遍歷等計算機科學基礎知識。

也許每一本講授編程語言的書,都應該借鑑《The AWK Programming Language》的寫法。

相關文章
相關標籤/搜索