awk入門教程

一. AWK入門指南

Awk是一種便於使用且表達能力強的程序設計語言,可應用於各種計算和數據處理任務。本章是個入門指南,讓你能夠儘快地開始編寫你自己的程序。第二章將描述整個語言,而剩下的章節將向你展示如何使用Awk來解決許多不同方面的問題。縱觀全書,我們儘量選擇了一些對你有用、有趣並且有指導意義的實例。

1.1 起步

有用的awk程序往往很簡短,僅僅一兩行。假設你有一個名爲 emp.data 的文件,其中包含員工的姓名、薪資(美元/小時)以及小時數,一個員工一行數據,如下所示:

Beth 4.00 0
Dan 3.75 0
kathy 4.00 10
Mark 5.00 20
Mary 5.50 22
Susie 4.25 18

現在你想打印出工作時間超過零小時的員工的姓名和工資(薪資乘以時間)。這種任務對於awk來說就是小菜一碟。輸入這個命令行就可以了::

awk '$3 >0 { print $1, $2 * $3 }' emp.data

你應該會得到如下輸出:

Kathy 40
Mark 100
Mary 121
Susie 76.5

該命令行告訴系統執行引號內的awk程序,從輸入文件 emp.data 獲取程序所需的數據。引號內的部分是個完整的awk程序,包含單個模式-動作語句。模式 $3>0 用於匹配第三列大於0的輸入行,動作:

{ print $1, $2 * $3 }

打印每個匹配行的第一個字段以及第二第三字段的乘積。

如果你想打印出還沒工作過的員工的姓名,則輸入命令行::

awk '$3 == 0 { print $1 }' emp.data

這裏,模式 $3 == 0 匹配第三個字段等於0的行,動作:

{ print $1 }

打印該行的第一個字段。

當你閱讀本書時,應該嘗試執行與修改示例程序。大多數程序都很簡短,所以你能快速理解awk是如何工作的。在Unix系統上,以上兩個事務在終端裏看起來是這樣的:

$ awk ‘$3 > 0 { print $1, $2 * $3 }’ emp.data
Kathy 40
Mark 100
Mary 121
Susie 76.5
$ awk ‘$3 == 0 { print $1 }’ emp.data
Beth
Dan
$

行首的 $ 是系統提示符,也許在你的機器上不一樣。

AWK程序的結構

讓我們回頭看一下到底發生了什麼事情。上述的命令行中,引號之間的部分是awk編程語言寫就的程序。本章中的每個awk程序都是一個或多個模式-動作語句的序列:

pattern { action }
pattern { action }
...

awk的基本操作是一行一行地掃描輸入,搜索匹配任意程序中模式的行。詞語“匹配”的準確意義是視具體的模式而言,對於模式 $3 >0 來說,意思是“條件爲真”。

每個模式依次測試每個輸入行。對於匹配到行的模式,其對應的動作(也許包含多步)得到執行,然後讀取下一行並繼續匹配,直到所有的輸入讀取完畢。

上面的程序都是模式與動作的典型示例。:

$3 == 0 { print $1 }

是單個模式-動作語句;對於第三個字段爲0的每行,打印其第一個字段。

模式-動作語句中的模式或動作(但不是同時兩者)都可以省略。如果某個模式沒有動作,例如::

$3 == 0

那麼模式匹配到的每一行(即,對於該行,條件爲真)都會被打印出來。該程序會打印 emp.data 文件中第三個字段爲0的兩行

Beth 4.00 0
Dan 3.75 0

如果有個沒有模式的動作,例如::

{ print $1 }

那麼這種情況下的動作會打印每個輸入行的第一列。

由於模式和動作兩者任一都是可選的,所以需要使用大括號包圍動作以區分於其他模式。

執行AWK程序

執行awk程序的方式有多種。你可以輸入如下形式的命令行::

awk 'program' input files

從而在每個指定的輸入文件上執行這個program。例如,你可以輸入::

awk '$3 == 0 { print $1 }' file1 file2

打印file1和file2文件中第三個字段爲0的每一行的第一個字段。

你可以省略命令行中的輸入文件,僅輸入::

awk 'program'

這種情況下,awk會將program應用於你在終端中接着輸入的任意數據行,直到你輸入一個文件結束信號(Unix系統上爲control-d)。如下是Unix系統的一個會話示例:

$ awk ‘$3 == 0 { print $1 }’
Beth 4.00 0

Beth

Dan 3.75 0

Dan

Kathy 3.75 10
Kathy 3.75 0

Kathy

...

加粗的字符是計算機打印的。

這個動作非常便於嘗試awk:輸入你的程序,然後輸入數據,觀察發生了什麼。我們再次鼓勵你嘗試這些示例並進行改動。

注意命令行中的程序是用單引號包圍着的。這會防止shell解釋程序中 $ 這樣的字符,也允許程序的長度超過一行。

當程序比較短小(幾行的長度)的時候,這種約定會很方便。然而,如果程序較長,將程序寫到一個單獨的文件中會更加方便。假設存在程序 progfile ,輸入命令行::

awk -f progfile     optional list of input files

其中 -f 選項指示awk從指定文件中獲取程序。可以使用任意文件名替換 progfile

錯誤

如果你的awk程序存在錯誤,awk會給你一段診斷信息。例如,如果你打錯了大括號,如下所示::

awk '$3 == 0 [ print $1 }' emp.data

你會得到如下信息:

awk: syntax error at source line 1
context is
$3 == 0 >>> [ <<<
extra }
missing ]
awk: bailing out at source line 1

“Syntax error”意味着在 >>> <<< 標記的地方檢測到語法錯誤。“Bailing out”意味着沒有試圖恢復。有時你會得到更多的幫助-關於錯誤是什麼,比如大括號或括弧不匹配。

因爲存在句法錯誤,awk就不會嘗試執行這個程序。然而,有些錯誤,直到你的程序被執行纔會檢測出來。例如,如果你試圖用零去除某個數,awk會在這個除法的地方停止處理並報告輸入行的行號以及在程序中的行號(這話是什麼意思?難道輸入行的行號是忽略空行後的行號?)。

1.2 簡單輸出

這一節接下來的部分包含了一些短小,典型的awk程序,基於操縱上文中提到的emp.data 文件. 我們會簡單的解釋程序在做什麼,但這些例子主要是爲了介紹awk 中常見的一些簡單有用的操作 – 打印字段, 選擇輸入, 轉換數據. 我們並沒有展現 awk 程序能做的所有事情, 也並不打算深入的去探討例子中的一些細節.但在你讀完這一節之後, 你將能夠完成一些簡單的任務, 並且你將發現在閱讀後面章節的時候會變的容易的多.

我們通常只會列出程序部分, 而不是整個命令行. 在任何情況下, 程序都可以用引號包含起來放到 awk 命令的地一個參數中運行, 就像上文中展示的那樣, 或者把它放到一個文件中使用 awk 的 -f 參數調用它.

在 awk 中僅僅只有兩種數據類型: 數值 和 字符構成的字符串. emp.data 是一個包含這類信息的典型文件 – 混合了被空格和(或)製表符分割的數字和詞語.

Awk 程序一次從輸入文件的中讀取一行內容並把它分割成一個個字段, 通常默認情況下, 一個字段是一個不包含任何空格或製表符的連續字符序列. 當前輸入的行中的地一個字段被稱做 $1, 第二個是 $2, 以此類推. 整個行的內容被定義爲 $0. 每一行的字段數量可以不同.

通常, 我們要做的僅僅只是打印出每一行中的某些字段, 也許還要做一些計算.這一節的程序基本上都是這種形式.

打印每一行

如果一個動作沒有任何模式, 這個動作會對所有輸入的行進行操作. print 語句用來打印(輸出)當前輸入的行, 所以程序

{ print }

會輸出所有輸入的內容到標準輸出. 由於 $0 表示整行,

{ print $0 }

也會做一樣的事情.

打印特定字段

使用一個 print 語句可以在同一行中輸出不止一個字段. 下面的程序輸出了每行輸入中的第一和第三個字段

{ print $1, $3 }

使用 emp.data 作爲輸入, 它將會得到

Beth 0
Dan 0
Kathy 10
Mark 20
Mary 22
Susie 18

print 語句中被逗號分割的表達式, 在默認情況下他們將會用一個空格分割來輸出. 每一行 print 生成的內容都會以一個換行符作爲結束. 但這些默認行爲都可以自定義; 我們將在第二章中介紹具體的方法.

NF, 字段數量

很顯然你可能會發現你總是需要通過 $1, $2 這樣來指定不同的字段, 但任何表達式都可以使用在$之後來表達一個字段的序號; 表達式會被求值並用於表示字段序號. Awk會對當前輸入的行有多少個字段進行計數, 並且將當前行的字段數量存儲在一個內建的稱作 NF 的變量中. 因此, 下面的程序

{ print NF, $1, $NF }

會依次打印出每一行的字段數量, 第一個字段的值, 最後一個字段的值.

計算和打印

你也可以對字段的值進行計算後再打印出來. 下面的程序

{ print $1, $2 * $3 }

是一個典型的例子. 它會打印出姓名和員工的合計支出(以小時計算):

Beth 0
Dan 0
Kathy 40
Mark 100
Mary 121
Susie 76.5

我們馬上就會學到怎麼讓這個輸出看起來更漂亮.

打印行號

Awk提供了另一個內建變量, 叫做 NR, 它會存儲當前已經讀取了多少行的計數.我們可以使用 NR $0emp.data 的沒一行加上行號:

{ print NR, $0 }

打印的輸出看起來會是這樣:

1 Beth   4.00     0
2 Dan    3.75     0
3 Kathy  4.00    10
4 Mark   5.00    20
5 Mary   5.50    22
6 Susie  4.25   1 8

在輸出中添加內容

你當然也可以在字段中間或者計算的值中間打印輸出想要的內容:

{ print "total pay for", $1, "is", $2 * $3 }

輸出

total pay for Beth is 0
total pay for Dan is 0
total pay for Kathy is 40
total pay for Mark is 100
total pay for Mary is 121
total pay for Susie is 76.5

在打印語句中, 雙引號內的文字將會在字段和計算的值中插入輸出.

1.3 高級輸出

print 語句可用於快速而簡單的輸出。若要嚴格按照你所想的格式化輸出,則需要使用 printf 語句。正如我將在2.4節所見, printf 幾乎可以產生任何形式的輸出,但在本節中,我們僅展示其部分功能。

字段排隊

printf 語句的形式如下::

printf(format, value1, value2, ..., valuen)

其中 format 是字符串,包含要逐字打印的文本,穿插着 format 之後的每個值該如何打印的規格(specification)。一個規格是一個 % 符,後面跟着一些字符,用來控制一個 value 的格式。第一個規格說明如何打印 value1 ,第二個說明如何打印 value2 ,... 。因此,有多少 value 要打印,在 format 中就要有多少個 % 規格。

這裏有個程序使用 printf 打印每位員工的總薪酬::

{ printf("total pay for %s is $%.2f\n", $1, $2 * $3) }

printf 語句中的規格字符串包含兩個 % 規格。第一個是 %s ,說明以字符串的方式打印第一個值 $1 。第二個是 %.2f ,說明以數字的方式打印第二個值 $2*$3 ,並保留小數點後面兩位。規格字符串中其他東西,包括美元符號,僅逐字打印。字符串尾部的 \n 代表開始新的一行,使得後續輸出將從下一行開始。以 emp.data 爲輸入,該程序產生:

total pay for Beth is $0.00
total pay for Dan is $0.00
total pay for Kathy is $40.00
total pay for Mark is $100.00
total pay for Mary is $121.00
total pay for Susie is $76.50

printf 不會自動產生空格或者新的行,必須是你自己來創建,所以不要忘了 \n

另一個程序是打印每位員工的姓名與薪酬::

{ printf("%-8s $%6.2f\n", $1, $2 * $3) }

第一個規格 %-8s 將一個姓名以字符串形式在8個字符寬度的字段中左對齊輸出。第二個規格 %6.2f 將薪酬以數字的形式,保留小數點後兩位,在6個字符寬度的字段中輸出。

Beth     $  0.00
Dan      $  0.00
Kathy    $ 40.00
Mark     $100.00
Mary     $121.00
Susie    $ 76.50

之後我們將展示更多的 printf 示例。一切精彩盡在2.4小節。

排序輸出

假設你想打印每位員工的所有數據,包括他或她的薪酬,並以薪酬遞增的方式進行排序輸出。最簡單的方式是使用awk將每位員工的總薪酬置於其記錄之前,然後利用一個排序程序來處理awk的輸出。Unix上,命令行如下:

awk '{ printf("%6.2f    %s\n", $2 * $3, $0) }' emp.data | sort

將awk的輸出通過管道傳給 sort 命令,輸出爲:

  0.00    Beth  4.00 0
  0.00    Dan   3.75 0
 40.00    Kathy 4.00 10
 76.50    Susie 4.25 18
100.00    Mark  5.00 20
121.00    Mary  5.50 22

1.4 選擇

Awk的模式適合用於爲進一步的處理從輸入中選擇相關的數據行。由於不帶動作的模式會打印所有匹配模式的行,所以很多awk程序僅包含一個模式。本節將給出一些有用的模式示例。

通過對比選擇

這個程序使用一個對比模式來選擇每小時賺5美元或更多的員工記錄,也就是,第二個字段大於等於5的行::

$2 >= 5

emp.data 中選出這些行::

Mark    5.00    20
Mary    5.50    22

通過計算選擇

程序

$2 * $3 > 50 { printf("$%.2f for %s\n", $2 * $3, $1) }

打印出總薪資超過50美元的員工的薪酬。

通過文本內容選擇

除了數值測試,你還可以選擇包含特定單詞或短語的輸入行。這個程序會打印所有第一個字段爲 Susie 的行::

$1 == "Susie"

操作符 == 用於測試相等性。你也可以使用稱爲 正則表達式 的模式查找包含任意字母組合,單詞或短語的文本。這個程序打印任意位置包含 Susie 的行::

/Susie/

輸出爲這一行::

Susie   4.25    18

正則表達式可用於指定複雜的多的模式;2.1節將會有全面的論述。

模式組合

可以使用括號和邏輯操作符與 && , 或 || , 以及非 ! 對模式進行組合。程序:

$2 >= 4 || $3 >= 20

會打印 $2 (第二個字段) 大於等於 4 或者 $3 (第三個字段) 大於等於 20 的行::

Beth    4.00    0
kathy   4.00    10
Mark    5.00    20
Mary    5.50    22
Susie   4.25    18

兩個條件都滿足的行僅打印一次。與如下包含兩個模式程序相比::

$2 >= 4
$3 >= 20

如果某個輸入行兩個條件都滿足,這個程序會打印它兩遍::

Beth    4.00    0
Kathy   4.00    10
Mark    5.00    20
Mark    5.00    20
Mary    5.50    22
Mary    5.50    22
Susie   4.25    18

注意如下程序:

!($2 < 4 && $3 < 20)

會打印極不滿足 $2 小於4也不滿足 $3 小於20的行;這個條件與上面第一個模式組合等價,雖然也許可讀性差了點。

數據驗證

實際的數據中總是會存在錯誤的。在數據驗證-檢查數據的值是否合理以及格式是否正確-方面,Awk是個優秀的工具。

數據驗證本質上是否定的:不是打印具備期望屬性的行,而是打印可疑的行。如下程序使用對比模式將5個數據合理性測試應用於 emp.data 的每一行::

NF != 3     { print $0, "number of fields is not equal to 3" }
$2 < 3.35   { print $0, "rate is below minimum wage" }
$2 > 10     { print $0, "rate exceeds $10 per hour" }
$3 < 0      { print $0, "negative hours worked" }
$3 > 60     { print $0, "too many hours worked" }

如果沒有錯誤,則沒有輸出。

BEGIN與END

特殊模式 BEGIN 用於匹配第一個輸入文件的第一行之前的位置, END 則用於匹配處理過的最後一個文件的最後一行之後的位置。這個程序使用 BEGIN 來輸出一個標題::

BEGIN { print "Name    RATE    HOURS"; print ""}
      { print }

輸出爲::

NAME    RATE    HOURS

Beth    4.00    0
Dan     3.75    0
Kathy   4.00    10
Mark    5.00    20
Mary    5.50    22
Susie   4.25    18

程序的動作部分你可以在一行上放多個語句,不過要使用分號進行分隔。注意 普通的 print 是打印當前輸入行,與之不同的是 print “” 會打印一個空行。

1.5 使用AWK進行計算

一個動作就是一個以新行或者分號分隔的語句序列。你已經見過一些其動作僅是單個 print 語句的例子。本節將提供一些執行簡單的數值以及字符串計算的語句示例。在這些語句中,你不僅可以使用像 NF 這樣的內置變量,還可以創建自己的變量用於計算、存儲數據諸如此類的操作。awk中,用戶創建的變量不需要聲明。

計數

這個程序使用一個變量 emp 來統計工作超過15個小時的員工的數目::

$3 > 15 { emp = emp + 1 }
END     { print emp, "employees worked more than 15 hours" }

對於第三個字段超過15的每行, emp 的前一個值加1。以 emp.data 爲輸入,該程序產生::

3 employees worked more than 15 hours

用作數字的awk變量的默認初始值爲0,所以我們不需要初始化 emp

求和與平均值

爲計算員工的數目,我們可以使用內置變量 NR ,它保存着到目前位置讀取的行數;在所有輸入的結尾它的值就是所讀的所有行數。

END { print NR, "employees" }

輸出爲::

6 employees

如下是一個使用 NR 來計算薪酬均值的程序::

    { pay = pay + $2 * $3 }
END { print NR, "employees"
      print "total pay is", pay
      print "average pay is", pay/NR
    }

第一個動作累計所有員工的總薪酬。 END 動作打印出

6 employees
total pay is 337.5
average pay is 56.25

很明顯, printf 可用來產生更簡潔的輸出。並且該程序也有個潛在的錯誤:在某種不太可能發生的情況下, NR 等於0,那麼程序會試圖執行零除,從而產生錯誤信息。

處理文本

awk的優勢之一是能像大多數語言處理數字一樣方便地處理字符串。awk變量可以保存數字也可以保存字符串。這個程序會找出時薪最高的員工::

$2 > maxrate { maxrate = $2; maxemp = $1 }
END { print "highest hourly rate:", maxrate, "for", maxemp }

輸出

highest hourly rate: 5.50 for Mary

這個程序中,變量 maxrate 保存着一個數值,而變量 maxemp 則是保存着一個字符串。(如果有幾個員工都有着相同的最大時薪,該程序則只找出第一個。)

字符串連接

可以合併老字符串來創建新字符串。這種操作稱爲 連接(concatenation) 。程序

    { names = names $1 " "}
END { print names }

通過將每個姓名和一個空格附加到變量 names 的前一個值, 來將所有員工的姓名收集進單個字符串中。最後 END 動作打印出 names 的值::

Beth Dan Kathy Mark Mary Susie

awk程序中,連接操作的表現形式是將字符串值一個接一個地寫出來。對於每個輸入行,程序的第一個語句先連接三個字符串: names 的前一個值、當前行的第一個字段以及一個空格,然後將得到的字符串賦值給 names 。因此,讀取所有的輸入行之後, names 就是個字符串,包含所有員工的姓名,每個姓名後面跟着一個空格。用於保存字符串的變量的默認初始值是空字符串(也就是說該字符串包含零個字符),因此這個程序中的 names 不需要顯式初始化。

打印最後一個輸入行

雖然在 END 動作中 NR 還保留着它的值,但 $0 沒有。程序

    { last = $0 }
END { print last }

是打印最後一個輸入行的一種方式::

Susie   4.25    18

內置函數

我們已看到awk提供了內置變量來保存某些頻繁使用的數量,比如:字段的數量和輸入行的數量。類似地,也有內置函數用來計算其他有用的數值。除了平方根、對數、隨機數諸如此類的算術函數,也有操作文本的函數。其中之一是 length ,計算一個字符串中的字符數量。例如,這個程序會計算每個人的姓名的長度::

{ print $1, length($1) }

結果::

Beth 4
Dan 3
Kathy 5
Mark 4
Mary 4
Susie 5

行、單詞以及字符的計數

這個程序使用了 lengthNF 、以及 NR 來統計輸入中行、單詞以及字符的數量。爲了簡便,我們將每個字段看作一個單詞。

    { nc = nc + length($0) + 1
      nw = nw + NF
    }
END { print NR, "lines,", nw, "words,", nc, "characters" }

文件 emp.data 有:

6 lines, 18 words, 77 characters

$0 並不包含每個輸入行的末尾的換行符,所以我們要另外加個1。

1.6 控制語句

Awk爲選擇提供了一個 if-else 語句,以及爲循環提供了幾個語句,所以都效仿C語言中對應的控制語句。它們僅可以在動作中使用。

if-else語句

如下程序將計算時薪超過6美元的員工的總薪酬與平均薪酬。它使用一個 if 來防範計算平均薪酬時的零除問題。

$2 > 6 { n = n + 1; pay = pay + $2 * $3 }
END    { if (n > 0)
            print n, "employees, total pay is", pay,
                     "average pay is", pay/n
         else
             print "no employees are paid more than $6/hour"
        }

emp.data 的輸出是::

no employees are paid more than $6/hour

if-else 語句中,`if` 後的條件會被計算。如果爲真,執行第一個 print 語句。否則,執行第二個 print 語句。注意我們可以使用一個逗號將一個長語句截斷爲多行來書寫。

while語句

一個 while 語句有一個條件和一個執行體。條件爲真時執行體中的語句會被重複執行。這個程序使用公式 value=amount(1+rate)years

來演示以特定的利率投資一定量的錢,其數值是如何隨着年數增長的。

# interest1 - 計算複利
#   輸入: 錢數    利率    年數
#   輸出: 複利值

{   i = 1
    while (i <= $3) {
        printf("\t%.2f\n", $1 * (1 + $2) ^ i)
        i = i + 1
    }
}

條件是 while 後括弧包圍的表達式;循環體是條件後大括號包圍的兩個表達式。 printf 規格字符串中的 \t 代表製表符; ^ 是指數操作符。從 # 開始到行尾的文本是註釋,會被awk忽略,但能幫助程序的讀者理解程序做的事情。

你可以爲這程序輸入三個一組的數字,看看不一樣的錢數、利率、以及年數會產生什麼。例如,如下事務演示了1000美元,利率爲6%與12%,5年的複利分別是如何增長的::

$ awk -f interest1
1000 .06 5
        1060.00
        1123.60
        1191.02
        1262.48
        1338.23
1000 .12 5
        1120.00
        1254.40
        1404.93
        1573.52
        1762.34

for語句

另一個語句, for ,將大多數循環都包含的初始化、測試、以及自增壓縮成一行。如下是之前利息計算的 for 版本::

# interest1 - 計算複利
#   輸入: 錢數    利率    年數
#   輸出: 每年末的複利

{ for (i = 1; i <= $3; i = i + 1)
    printf("\t%.2f\n", $1 * (1 + $2) ^ i)
}

初始化 i = 1 只執行一次。接下來,測試條件 i <= $3 ;如果爲真,則執行循環體的 printf 語句。循環體執行結束後執行自增 i = i + 1 ,接着由另一次條件測試開始下一個循環迭代。代碼更加緊湊,並且由於循環體僅是一條語句,所以不需要大括號來包圍它。

1.7 數組

awk爲存儲一組相關的值提供了數組。雖然數組給予了awk很強的能力,但在這裏我們僅展示一個簡單的例子。如下程序將按行逆序打印輸入。第一個動作將輸入行存爲數組 line 的連續元素;即第一行放在 line[1] ,第二行放在 line[2] , 依次繼續。 END 動作使用一個 while 語句從後往前打印數組中的輸入行::

# 反轉 - 按行逆序打印輸入

    { line[NR] = $0 }  # 記下每個輸入行

END { i = NR           # 逆序打印
      while (i > 0) {
        print line[i]
        i = i - 1
      }
    }

emp.data 爲輸入,輸出爲

Susie    4.25   18
Mary     5.50   22
Mark     5.00   20
Kathy    4.00   10
Dan      3.75   0
Beth     4.00   0

如下是使用 for 語句實現的相同示例::

# 反轉 - 按行逆序打印輸入

    { line[NR] = $0 }   # 記下每個輸入行

END { for (i = NR; i > 0; i = i - 1)
        print line[i]
    }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章