全文6,829字(含代碼),閱讀18分鐘。

在掌握了上一篇文章中 awk 基礎用法的之後,這一篇文章我們來進一步深入地理解和應用 awk。

理解AWK的工作原理

首先,第一個應該加深理解的地方就是 awk 的工作原理(或者說是執行流程)。理解了其工作原理本身,也有助於我們寫出更好的 awk 。下面這個圖來自 runoob.com 上一篇關於 awk 的文章,它非常清楚明白地描述出了 awk 的工作原理和執行流程,可以說理解 awk 的原理看這一張圖幾乎就足夠了(下圖)。

圖源:runoob.com

總的來說,awk 的執行流程可以分成三個大的部分:

  • 讀輸入文件之前需要執行的代碼段,由 BEGIN 關鍵字所標識;
  • BODY塊,這裡是自動循環並處理輸入文件的代碼段,也是我們處理數據的核心之處,默認情況下,我們編寫的 awk 其實都是BODY塊
  • 讀取並處理了全部輸入文件的內容之後才執行的代碼段,由 END 關鍵字所標識。

命令的結構如下:

$ awk BEGIN{動作} pattern{動作} END{動作}

這裡的 pattern 屬於BODY塊,你可以寫上一些正則表達式或者條件判斷語句,雖然這些語句也可以在 大括弧{} 里正式的BODY塊中完成,但是寫在外面可以使整個命令看起來更加清爽。如:

$ awk BEGIN{OFS=" ";print "#CHROM POS INFO"} $1!~/^#/ && $6>40 {print $1,$2,$8} demo.vcf
#CHROM POS INFO
chr22 17662679 CMDB_AF=0.030044,CMDB_AC=420,CMDB_AN=13442
chr22 17662699 CMDB_AF=0.031047,CMDB_AC=441,CMDB_AN=13553
chr22 17662699 CMDB_AF=0.031047,CMDB_AC=441,CMDB_AN=13553
chr22 17662793 CMDB_AF=0.050419,CMDB_AC=842,CMDB_AN=16135
chr22 17662793 CMDB_AF=0.050419,CMDB_AC=842,CMDB_AN=16135
chr22 17663076 CMDB_AF=0.053564,CMDB_AC=534,CMDB_AN=9525

上面的語句就是這樣的一個例子,BEGIN 中設定了輸出內容的表頭和輸出分隔符,然後是 pattern,接著是BODY塊的主程序。

所以,awk 的工作原理和執行流程是這樣的:

  • 1. 在所有處理操作之前,先讀取 BEGIN 關鍵字標識起來的代碼段,並執行之,給一些預設變數賦值或者輸出表頭信息;
  • 2. 然後執行 BODY 塊,一行一行往下完成文本的處理;
  • 3. 在 BODY 執行過程中,對每一行,按照指定的分隔符,把當前整行的內容進行切分,並填充到 awk 內置的數據域中,如 $0 標示所有數據域(也就是原來的行內容),$1 表示第一個域,$n 表示第 n 個域;
  • 4. 如果 BODY 前有 pattern 匹配和條件判斷語句,那麼在依次執行時,只有符合 pattern 條件的才會執行 BODY 中的動作;
  • 5. 循環讀取到整個文件結束之後,就完成了 BODY 塊的執行;
  • 6. 執行 END 代碼段,在 END 塊中完成最終結果的輸出。

自定義變數

在看過上一篇文章之後,我想大家一定還多少還記得 awk 的內置變數(比如 NF,FS,OFS等),它們可以幫助我們完成很多的事情。但是內置的變數畢竟是固定的,缺乏靈活性,有些操作它們就不能夠勝任了,特別是當我們需要從外部傳入參數的時候,它們就通通都不好使了。這個時候我們就需要有一個能夠自定義變數的方式,-v 參數在 awk 中就是用於補足這一個需求的,它是這樣使用的:

$ awk -v 變數名字和賦值 {動作} 文件名

來一個實際的例子:

$ awk -v qual=40 $1!~/^#/ && $6>qual {print $1,$2,$8} demo.vcf
chr22 17662679 CMDB_AF=0.030044,CMDB_AC=420,CMDB_AN=13442
chr22 17662699 CMDB_AF=0.031047,CMDB_AC=441,CMDB_AN=13553
chr22 17662699 CMDB_AF=0.031047,CMDB_AC=441,CMDB_AN=13553
chr22 17662793 CMDB_AF=0.050419,CMDB_AC=842,CMDB_AN=16135
chr22 17662793 CMDB_AF=0.050419,CMDB_AC=842,CMDB_AN=16135
chr22 17663076 CMDB_AF=0.053564,CMDB_AC=534,CMDB_AN=9525

在上面這個例子裏,我們通過 -v 參數設置一個自定義變數 qual 並給它賦值為 40, 然後在BODY主程序中 qual 被用於一個條件判斷語句,把符合這個條件的 demo.vcf 內容輸出出來,非常方便。而且對於自定義變數來說,最大的一個好處是,讓 awk 可以和外部進行充分交互,通過接受外部參數,完成內部動作

而且 -v 還可以多重設置,把多個變數輸入到 awk 執行代碼段之中,這真的是一個很有用功能。如:

$ awk -v qual=40 -v pos=17662793 $1!~/^#/ && $6>qual && $2>pos {print $1,$2,$8} demo.vcf
chr22 17663076 CMDB_AF=0.053564,CMDB_AC=534,CMDB_AN=9525

在上面這個命令裡面,我不但通過自定義參數要求 $6 > qual,還同時要求只輸出那些 $2 > pos 的結果。你如果有更多的需要,可以不斷往後加上 -v 設置變數。

數組

awk 中也有數組的概念和數據組織形式,不過與其說是數組,不如說更像是哈希表,原因是它的數組索引可以不必像通常我們所知的那樣。

首先,它的數組語法格式這樣的:

array_name[index] = value

其中:

  • array_name 是數組的名稱;
  • index是數組的索引,這個索引可以是數字下標也可以是字元下標;
  • value是數組中元素的值

接下來,我們先看一下應該如何創建和訪問數組中的元素:

$ awk BEGIN{sites["chrom"]="chr22"; sites["pos"]=17662679; print sites["chrom"], sites["pos"]}

這個命令執行之後,print出來的結果是:

chr22 17662679

在上面代碼中,我定義了一個名字為 sites 的數組,這個數組的索引下標我不是用通常的數字,而是字元——後面再舉例子講數字下標,這個做法與哈希表如出一轍(或者說,就是哈希)。用字元索引代替數字索引的好處是,可以用名稱來獲得對應的 value,建立起索引和 value 之間的一個映射關係,甚至可以像哈希表那樣通過 index 進行信息查找。

這個方式還可以 「人為地」 製造出多維數組。只需要你把索引的命名按照多維數組那樣的形式來進行就可以。比如,以一個二維數組為例,我們可以用 array_name["0,0"]、array_name["0,1"]、 array_name["1,0"]、array_name["1,1"]分別代表一個 2×2 數組中的各個元素,這裡就不額外舉例子了。

以上是字元下標的數組,接著我舉一個數字下標的數組例子:

$ echo "this is a variant in vcf file" | awk {split($0, array, " "); for(i=1; i<=length(array); i++){print array[i]} }
this
is
a
variant
in
vcf
file

在這個例子裡面,我想你也可以看出來,數字下標的數組一般都是通過文本處理而產生的,比如這裡我就是通過 split 函數,把 「this is a variant in vcf file」 這一個字元串,按照空格,將它切分為一個數組,數組中的元素為這字元串中的每一個單詞。然後,再寫一個循環語句將其輸出(循環語句中 length函數,可以獲取到該數組的長度),值得注意的一個地方是,awk 數組的第一個元素下標是 1 而不是 0

另外,如果要刪除掉數組中的某個元素,只需要通過 delete 語句就可以實現,語法:

delete array_name[index]

這樣就可以隨意把任意一個 index 索引的元素刪除掉。

其實,awk 的數組功能,我們在生物信息數據分析的場景中用的不多,就算真要用到,這個分析任務的複雜性也往往不是在 awk 僅用數組就可以解決的,這個時候可能也是需要寫成腳本的時候了。但不管如何,數組的創建和使用方法還是值得在這裡描述清楚的。特別是在數組上也可以有更多的操作,比如,還可以用 asort 對數據元素進行排序,或者使用 asorti 對數組索引進行排序。

再談條件判斷與循環語句

awk 雖然是一個 文本文件處理程序,但其實它也像是一個編程語言,所以在常見編程語言中該有的功能和語法表達形式,其實它也照樣有。比如,之前提到的 if - else 語句,這裡我還要再說上一說,同時也把循環語句補充上來。

先說 if 的語法:

if (條件) {
動作
}

中間的執行動作,都括在大括弧裏。由於之前(見上一篇文章)已經給過不少例子了,所以這裡我想偷個懶,只要大家能夠看明白的,就不多舉例子了。

除了 if 語句,緊接著的就是 if-else 語句,它的語法結構是:

if (條件) {
動作
} else {
動作
}

if 中的判斷條件符合了,就執行 if 中的動作,否則執行 else 中的動作,這是一個比較常用的語句功能。

除了上面兩種之外,其實 awk 也有 if-else-if 語句,我們可以用它來創建多個 if-else 組合,實現多條件判斷。

if (條件1){
動作
} else if (條件2) {
動作
} else if (條件3) {
動作
} else {
動作
}

關於 awk 的 if 語句就在這裡都補充完成了。接下來說一說,awk 中的另一個重要語句:循環。

循環也是常規編程語言用有的核心語法,在 awk 中也不例外。雖然,awk 在處理文本數據的時候,BODY 語句會自動循環執行的,但是它的循環是在文本文件中一行行往下進行的循環。如果我們需要在每一行文本處理中都做出一些其他的循環操作,那麼就需要使用 awk 提供出來的循環語句。

awk 的循環語句有兩種:for 和 while 。

對於 for 循環來說,它的語法是這樣的:

for (起始條件初始化; 終止條件; 迭代起始條件) {
動作
}

對於有過編程基礎的朋友來說,應該對這種結構非常熟悉,幾乎所有常見的編程語言,都是類似的for循環結構。它在執行的時候,先初始化起始條件,然後與終止條件比較,如果條件為真,那麼執行 for 循環中的動作——也就是執行循環體,然後執行第三部分「迭代起始條件」——這個迭代一般是遞增或者遞減操作,然後再繼續和終止條件進行比較,只要比較結果為真,就一直循環下去;直到條件為假,才終止 for 循環並退出這個執行語句。下面就是一個簡單的循環輸出數字的 awk 語句:

$ awk BEGIN{ for(i=0; i<4; i++){print i} }
1
2
3

之所以把這個語句中用在 BEGIN 裏,目的其實就是想省下對具體文件的處理,方便作為例子。至於在具體的項目中,還應該按照具體的文件處理需求來執行。

對於 while 循環來說,它的語法結構為:

while (終止條件) {
動作
}

相比於 for 循環語句,while 語句要簡單得多。它只檢查 while 後面的條件是否為真,如果是真,那麼執行,如果為假,那麼結束循環。這裡用數字輸出作為例子:

$ awk BEGIN{i=1; while(i<4){print i; ++i;} }
1
2
3

在 for 或者 while 循環中,並不是隻有等到終止條件為假的時候,纔可以退出循環。有時在執行的過程中,我們也可以強制中斷循環體或者跳過某一次循環。能夠完成這兩個功能的是 awk 循環中提供的 break 和 continue 語句,而且這兩個都是隻在循環體(執行動作的語句)中使用的語句。

break 語句可以讓我們在碰到某個條件的時候就強制退出循環,而 continue 語句則可以讓在碰到某個條件之後,直接忽略在 continue 之下的執行動作,直接回到循環頭進入下一次循環迭代。比如,我們用 continue 舉個例子,輸出所有 1-10 之間的奇數:

$ awk BEGIN{ for(i=1; i<=10; i++){ if(i % 2 > 0){print i;} else { continue; }} }
1
3
5
7
9

自定義函數

awk 中自定義函數的語句是 function ,使用這個語句,就越來越像是在編程了,雖然能夠做的事情更多了,但代價是整個 awk 也會因此變得更加複雜。

函數的好處,除了功能模塊化之外,就是提高代碼的復用性。在 awk 中我們自定義函數的語法是:

function function_name(參數1,參數2,參數3,...){
動作
}

其實跟前面的語句有類似之處,都是關鍵字+名稱+參數(或者判斷條件)+動作的模式。這裡函數前面的 function 關鍵字是必須,它規定了這是一個自定義的函數。其中:

  • function_name 是函數名字;
  • 大括弧括起來的一系列執行動作是該函數所要完成的具體功能

另外,函數的定義一般要在其它 awk 操作之前完成。我自己沒有合適的例子,就借用網上的一個 awk 函數來舉例吧。下面代碼定義了兩個功能很簡單的函數,它們分別用於數字比較之後,返回數據中的最小值和最大值,然後還定義了一個 main 函數作為主函數來調用它們。而且,一般來說,當需要自定義函數時,代碼都會比較長,已經不適合在一行命令中寫下,所以會寫成一份真正的 awk 腳本文件,這個文件的後綴用 .awk,比如這裡我們就可以將其命名為 function_demo.awk ,其中的所有 awk 代碼如下:

# 返回最小值
function find_min(num1, num2){
if (num1 < num2)
return num1
return num2
}
# 返回最大值
function find_max(num1, num2){
if (num1 > num2) {
return num1
} else {
return num2
}
}

# 主函數
function main(num1, num2){
# 查找最小值
result = find_min(num1, num2)
print "Minimum =", result

# 查找最大值
result = find_max(num1, num2)
print "Maximum =", result
}

# 整個腳本還是從這裡開始執行
BEGIN {
main(30, 20)
}

這時,通過 awk -f 執行這個腳本,我們就可以得到如下結果:

$ awk -f function_demo.awk
Minimum = 20
Maximum = 30

要再提醒大家的是,這個腳本里只定義了 BEGIN 代碼段,這是為了可以在不用有任何文件輸入時也能執行。但在實際使用的時候,我們是需要定義 BODY 代碼段的,甚至還有 END 代碼段的,並且在最後還要有一份待處理的文件作為輸入。

還能同時處理多個文件?

其實從 awk 本來的設計理念來看,它最適合的場景是一次只處理一份文件。但如果在某些情況下,我們非要同時處理多個文件,awk 也能做到,只是這個情況用的很少,而且也相對費勁一些。我自己從未如此使用過,它也不是本文的重點,所以這裡我也不打算進一步展開,只是想告訴大家 awk 是有能力這樣做的,大家真有需要了,再從網上或者它的手冊中找到它的具體用法吧。

小結

這篇文章就在這裡結束吧。如無意外這應該也是最近兩篇 awk 文章中的最後一篇,四千五百多字(不含代碼)。看完這一篇,再加上上一篇的 awk 基礎用法,我們其實已經可以用 awk 來實現很多工作了,包括很複雜的文本處理,都完全可以通過 awk 實現。但是,我覺得要提醒一下大家,awk 是動態語言,執行效率並不是很高,處理一些比較小的文件,確實沒有什麼問題。但,如果要處理大型的文件,比如 BAM 之類的,那麼不建議用 awk 。而且,awk 的功能畢竟還是比較單一,在處理多文件處理方面也不是很靈活,也不能很好地與其他代碼進行交互,更加沒有什麼基於 awk 開發的包來支持更多的分析,它本身是一把精緻的匕首,我們就不要過多地將其它當大刀來使。任何工具或者編程語言都應該是用在它最合適的地方上纔好,用不著因為手裡拿著一個鎚子,所以就要把世界都當成了釘子。對我來說,使用 awk 主要還是圖它在基本文本處理方面的簡單、方便和快捷,可以只用一行命令就搞定很多事情,如果複雜了我也不一定要用 awk 了

參考鏈接

runoob.com/w3cnote/awk-

runoob.com/w3cnote/awk-

如果喜歡更多的生物信息和組學文章,歡迎搜索並關注我的微信公眾號: helixminer

你還可以讀

  • 生物信息 awk 簡明教程和基本用法
  • 如何有效使用CMDB基因頻率資料庫
  • 一篇文章說清楚基因組結構性變異檢測的方法

推薦閱讀:

相關文章