博客 / 詳情

返回

AWK基礎教程

前言

之前針對WorkerHub小程序做的數據分析文章 互聯網卷王花落誰家? 收到了一些小夥伴的回覆,點名要學習數據分析,其實我也是一知半解,想着來寫幾篇文章簡單聊下我分析的過程。

首先是數據清洗和統計了,這塊我並沒有用諸如Python之類的腳本語言,雖然Python提供了很多強大的數據分析庫如Pandas、Numpy等,但是畢竟要麻煩一些,還要寫個腳本,裝一堆庫(PS:其實就是懶)。

我用的是一個老的Unix工具 AWK,雖然歷史比較久遠,但是它簡潔和豐富的功能可以稱之為神器,同時也是後台同學必須要掌握的一個工具,畢竟通過日誌緊急定位線上問題的時候,你不能跟老闆説:等等我先寫個腳本,那老闆原地暴斃了。

應用場景

AWK是1977年貝爾實驗室的三個兄弟( Alfred Aho、Peter Weinberger、 Brian Kernighan )搞出來的文本分析工具,這三個哥們的首字母拼起來就是AWK的名字了。

AWK處理文本就像其他語言處理數值一樣方便,所以經常被應用在文本處理領域。

比如日誌分析、數據清洗、文本過濾、數據統計等。

同時AWK也是一門編程語言,不過它的命令行用法就可以滿足大多數的應用場景。

我們通常可以使用一行AWK命令完成一個腳本的任務!!!

AWK所適用的文本處理通常都有一些共同&顯著的特點:

1. 輸入數據格式統一

比如日誌,為了對日誌進行上報、監控、統計分析,我們通常會採用一些分割手段來記錄日誌 (或者json等易於統計的格式)。

例如如下日誌採用"|"來分割日誌。

# 日誌格式:{服務}|{日期}|{業務}|{請求URL}|{返回狀態}|{請求耗時}|{請求參數}|{返回參數}...

比如CSV文件,採用","來分割。

# CSV格式:field1,field2,field3...

如果輸入數據不是固定格式,通常會使用sed、grep等工具來過濾、清洗為awk可以處理的形式。

2. 每一列代表固定含義,便於數據分析

輸入文件每一行的相同列類型一致,如果每一列含義不同,那就失去了數據分析的意義。

比如本文的第一個演示數據,第一列表示地區,第二列表示總人口等。

演示數據來源於國家統計局。

各地區户口登記地在外鄉鎮街道的人口狀況

由於演示數據文件行數太多佔用篇幅較長,以下演示均只展示前幾條數據。

$ cat population.txt|head -n 10
地區      合計      本縣/市/區  本省其他縣/市/區     省外
全國    260937942   90372599     84689006       85876337
北京    10498288    1582574      1871181        7044533
天津    4952225     1095282      865442         2991501
河北    8297279     4263957      2628649        1404673
山西    6764665     3643627      2189385        931653
內蒙古  7170889     2732591      2994117        1444181
遼寧    9310058     3899728      3623800        1786530
吉林    4462177     2604239      1401439        456499
黑龍江  5557828     2800727      2250704         506397

Let's start !!!

基本用法

一個AWK程序的組成非常簡單,它的核心內容是:一個或多個 "模式–動作" 語句序列。

"模式–動作" 序列用單引號包起來,動作放在花括號裏,再傳入輸入文件即可。

### 一個 模式-動作
awk 'pattern {action}' input_files

### 多個 模式-動作
awk 'pattern1 {action1} pattern2 {action2} pattern3 {action3} ...' input_files

AWK會每次讀取一個輸入行,對讀取到的每一行,按順序檢查每一個模式。

如果當前行符合模式,則執行對應動作。

所以AWK的工作原理就是按順序匹配模式然後執行動作。

可以想象到AWK偽代碼大概長這樣,我猜的(*^_^*)。

### AWK偽代碼  我猜的 (*^_^*)
while(getline(inputfile))
{
    if(模式1 == true)
    {
        動作1;
    }
    if(模式2 == true)
    {
        動作2;
    }
    ....
}

AWK在自動掃描輸入文件的同時, 也會按照分隔符(默認空格/Tab)把每一個輸入行切分成字段。

其中 $0 表示整行,$1,$2...$n 分別表示第一列,第二列...第N列。

大致的流程圖如下:

大部分的工作都是AWK自動完成的:包括按行輸入,字段分割,字段存儲等。

所以我們只需要給出 "模式–動作" 序列就可以完成對文件的操作!!!

來個 Hello World 吧,輸出 "hello" 和 整行 ($0)。

print 函數使用逗號分隔不同的參數,打印結果用空格符分隔,並且會自動換行。(類似於各大語言println函數)。

模式可以省略,表示匹配所有行。

$ awk '{print "hello",$0}' population.txt|head -n 5
hello 地區      合計      本縣/市/區  本省其他縣/市/區     省外
hello 全國    260937942   90372599     84689006       85876337
hello 北京    10498288    1582574      1871181        7044533
hello 天津    4952225     1095282      865442         2991501
hello 河北    8297279     4263957      2628649        1404673

AWK提供了很多有用的內置變量,如:

NR (Number Of Record) :表示讀取到的記錄數,即當前行號。

FILENAME :表示當前輸入的文件名。

NF (Number Of Field) :表示當前記錄的字段個數,即總共多少列。

我們通常用 $NF 提取當前行的最後一列。

如下例子所示,總共有5列,$NF代表的就是第五列的值,等價於$5,$(NF-1)表示倒數第二列的值。

$ awk '{print FILENAME,NR,$1,$3,NF,$NF}' population.txt|head -n 5
population.txt 1 地區 本縣/市/區 5 省外
population.txt 2 全國 90372599 5 85876337
population.txt 3 北京 1582574 5 7044533
population.txt 4 天津 1095282 5 2991501
population.txt 5 河北 4263957 5 1404673

常見的內建變量可以去附錄查閲:常見的內建變量 。

AWK也提供了格式化輸出函數,跟C語言的printf用法一樣。

$ awk '{printf "%s的外地總人口有:%d,省外人口有:%0.2f\n",$1,$2,$NF}' population.txt|tail -n 5
陝西的外地總人口有:5894416,省外人口有:974362.00
甘肅的外地總人口有:3112722,省外人口有:432833.00
青海的外地總人口有:1140954,省外人口有:318435.00
寧夏的外地總人口有:1534482,省外人口有:368451.00
新疆的外地總人口有:4276951,省外人口有:1791642.00

格式化規則可以參考:https://www.gnu.org/software/... 。

模式過濾

上面介紹了動作的使用,動作通常用來輸出展示。

模式用來過濾我們想要的記錄。

如下篩選(行號>1 且 第二列大於11074525)的行。

### AWK的變量也可以自由進行算術運算(加減乘除),比如 $2-$3
$ awk 'NR>1 && $2>11074525 {print NR,$1,$2,$2-$3}' population.txt
2 全國 260937942 170565343
11 上海 12685316 11016029
12 江蘇 18226819 13681789
13 浙江 19900863 15274032
17 山東 13698321 7123530
21 廣東 36806649 31390437
25 四川 11735152 6913850

AWK的字符串拼接跟shell一樣簡單粗暴,不需要使用任何運算符。

將兩個字符串並排放在一起就能實現拼接。

$ awk 'NR>1 {print NR,"開始_"$1"_結束"}' population.txt|head -n 5
2 開始_全國_結束
3 開始_北京_結束
4 開始_天津_結束
5 開始_河北_結束
6 開始_山西_結束

AWK還提供了很多有用的內置函數。

length(s):用來計算字符串s 的長度。

### 我的系統編碼 & 文件編碼均為UTF-8
$ awk 'length($1) > 6 {print $1,"佔用長度:",length($1)}' population.txt
內蒙古 佔用長度: 9
黑龍江 佔用長度: 9

substr(s,p):求字符串s的子串,從位置p開始到末尾。

$ awk '{print $1,substr($1,4)}' population.txt|head -n 5
地區 區
全國 國
北京 京
天津 津
河北 北

常見的內建函數可以去附錄查閲:常見的內建函數 。

AWK還提供了一些特殊的模式,比如 BEGIN 和 END。這兩個模式不匹配任何輸入行。

當 awk讀取數據前,BEGIN 的語句開始執行,通常用於初始化。

例如我們可以用BEGIN來給輸出打印一個表頭。

### 多個 "模式-動作" 並排寫就行。
$ awk 'BEGIN{print "AREA TOTAL LOCAL OTHER OUTLAND"} NR>2{print}' population.txt|head -n 5
AREA TOTAL LOCAL OTHER OUTLAND
北京    10498288    1582574      1871181        7044533
天津    4952225     1095282      865442         2991501
河北    8297279     4263957      2628649        1404673
山西    6764665     3643627      2189385        931653

當所有輸入行被處理完畢,END 的語句開始執行。通常用來收尾。

例如我們可以統計一下第二列大於262005的省份,並在END進行打印。

$ awk 'NR>2 && $2>262005{count += 1} END{print count"個大於262005的省份"}' population.txt
30個大於262005的省份

同一個動作裏的多個語句之間使用分號或者換行進行分割。

如下在BEGIN的動作中先指定輸出分隔符,接着打印表頭。

OFS (Output Formmat Separate) 也是一個內建變量:指定輸出字段分割符。

如下指定輸出時字段採用逗號進行分割。

$ awk 'BEGIN{OFS=",";print "AREA,TOTAL,LOCAL,OTHER,OUTLAND"} NR>2{print $1,$2,$3,$4,$5}' population.txt|head -n 5
AREA,TOTAL,LOCAL,OTHER,OUTLAND
北京,10498288,1582574,1871181,7044533
天津,4952225,1095282,865442,2991501
河北,8297279,4263957,2628649,1404673
山西,6764665,3643627,2189385,931653

AWK提供了範圍模式可以根據一個區間來匹配多個輸入行。

範圍模式由兩個被逗號分開的模式組成。

awk 'pattern1,pattern2 {action}' input_file

AWK從符合 pattern1 的行開始,到符合 pattern2 的行結束 (包括這兩行),對這其中的每一行執行action。

如下提取第五行到第十行之間地區的數據。

$ awk 'NR==5,NR==10" {print NR,$0}' population.txt
5 河北    8297279     4263957      2628649        1404673
6 山西    6764665     3643627      2189385        931653
7 內蒙古  7170889     2732591      2994117        1444181
8 遼寧    9310058     3899728      3623800        1786530
9 吉林    4462177     2604239      1401439        456499
10 黑龍江  5557828     2800727      2250704        506397

流程控制

前文提到了AWK也是一門編程語言,所以它支持很多編程語言特性,與C語言使用類似。

比如流程控制語句 if-else 、循環(for,while)。

比如數據結構數組等。

它們只能用在動作裏。

如下示例使用if-else統計第二列大於4462177 和小於4462177的分別有多少行。

$ awk 'NR>2{if($2>4462177) more+=1; else less+=1} END{print "more:",more,"less:",less}' population.txt
more: 24 less: 7

上面這個例子也可以拆分成多個"模式-動作"來實現。

$ awk 'NR>2 && $2>4462177{more+=1} NR>2 && $2<=4462177{less+=1} END{print "more:",more,"less:",less}' population.txt
more: 24 less: 7

再來看個for循環的例子,打印AWK的命令行參數。

命令行參數在輸入文件後追加就可以傳入。

$ awk 'BEGIN {for(i=0;i<ARGC;i++) printf "%s\t",ARGV[i]; print ""}' population.txt abc def cdg
awk    population.txt    abc    def    cdg

ARGC和ARGV也是AWK的內建變量,跟C語言的參數結構差不多。

ARGC:命令行參數的個數。

ARGV:命令行參數數組。

// 等價於C語言
int main(int argc, char *argv[])

AWK也支持使用數組進行數據存儲。

如下示例將對輸入行進行倒序輸出。

$ awk '{addr[NR]=$1} END{i=NR; while(i>0){print i,addr[i];i-=1}}' population.txt|head -n 5
33 新疆
32 寧夏
31 青海
30 甘肅
29 陝西

正則表達式

AWK 提供了對正則表達式的支持,正則表達式放在一對斜槓裏:/regexpr/ 。

AWK使用 "\~" 符號表示字符串匹配,"!\~" 符號表示不匹配。

所以我們可以在模式中判斷一個字符串是否匹配一個正則表達式。

如下示例對 第一列含有 “北” 且第二列不包含 “88” 的行 進行打印。

$ awk '$1 ~ /北/ {print}' population.txt
北京    10498288    1582574      1871181        7044533
河北    8297279     4263957      2628649        1404673
湖北    9250228     4445565      3791051        1013612

$ awk '$1 ~ /北/ && $2 !~ /88/ {print}' population.txt
河北    8297279     4263957      2628649        1404673
湖北    9250228     4445565      3791051        1013612

如果判斷整行是否匹配,可以省略 "~" 的左值,如下所示。

###  /regexpr/ 等價於 $0  ~ /regexpr/
### !/regexpr/ 等價於 $0 !~ /regexpr/
$ awk '!/西/ && /88/ {print}' population.txt
北京    10498288    1582574      1871181        7044533
內蒙古  7170889     2732591      2994117         1444181
福建    11074525    3162036      3598887        4313602
湖南    7898815     4170436      3003397        724982
海南    1843430     586432       668535         588463
青海    1140954     351988       470531         318435

正則表達式的語法細節本文不過多説明。

以下是幾個小例子可以參考:

### 匹配小寫字母開頭的字符串
$ awk '/^[a-z]/' <<< "`echo -e "apple333\n1999fds\nhaode3232\n4343...\nhaoya328"`"
apple333
haode3232
haoya328

### 驗證是否是11位國內手機號碼
$ awk '/^1[3584][0-9]{9}$/' <<< "`echo -e "18894465939\n1364483882\n13644838825\n23443243432\n1334funny"`"
18894465939
13644838825

進階用法

接下來換個內容豐富的數據集來演示。

以下是 豆瓣電影評分Top250 的 CSV數據集。

### 數據格式:排行,電影名,評分,年份,導演,標籤,星級
$ cat douban_top250.csv|head -n 5
rank,title,rating_num,year,director,quote,star
1,肖申克的救贖,9.7,1994,弗蘭克·德拉邦特 Frank Darabont,希望讓人自由。,2304569
2,霸王別姬,9.6,1993,陳凱歌 Kaige Chen,風華絕代。,1709820
3,阿甘正傳,9.5,1994,羅伯特·澤米吉斯 Robert Zemeckis,一部美國近現代史。,1733112
4,這個殺手不太冷,9.4,1994,呂克·貝鬆 Luc Besson,怪蜀黍和小蘿莉不得不説的故事。,1913405

AWK默認按照 空格/Tab 對每一個輸入行進行切分。

我們可以使用 -F 參數進行指定分隔符,也支持多個分隔符。

### 指定分隔符
$ awk -F',' '{print $1,$2,$3}' douban_top250.csv|head -n 3
rank title rating_num
1 肖申克的救贖 9.7
2 霸王別姬 9.6

### 多個分隔符 可以看到評分被切分了
$ awk -F'[,.]' '{print $1,$2,$3,$4}' douban_top250.csv|head -n 3
rank title rating_num year
1 肖申克的救贖 9 7
2 霸王別姬 9 6

AWK支持使用shell重定向運算符 > 和 >> ,可以對文件進行拆分。

如下將 評分9以上的另存為douban_more_9.csv,評分9以下的為douban_less_9.csv。

$ awk -F',' 'NR>1 && $3>=9 {print $0 > "douban_more_9.csv"} NR >1 && $3<9 {print $0 > "douban_less_9.csv"}' douban_top250.csv

$ cat douban_less_9.csv|head -n 5
61,讓子彈飛,8.9,2010,姜文 Wen Jiang,你給我翻譯翻譯,神馬叫做TMD的驚喜。,1294845
63,綠皮書,8.9,2018,彼得·法雷裏 Peter Farrelly,去除成見,需要勇氣。,1245160
65,本傑明·巴頓奇事,8.9,2008,大衞·芬奇 David Fincher,在時間之河裏感受溺水之苦。,788815
68,看不見的客人,8.8,2016,奧里奧爾·保羅 Oriol Paulo,你以為你以為的就是你以為的。,965038
69,西西里的美麗傳説,8.9,2000,朱塞佩·託納多雷 Giuseppe Tornatore,美麗無罪。,781719

$ cat douban_more_9.csv|head -n 5
1,肖申克的救贖,9.7,1994,弗蘭克·德拉邦特 Frank Darabont,希望讓人自由。,2304569
2,霸王別姬,9.6,1993,陳凱歌 Kaige Chen,風華絕代。,1709820
3,阿甘正傳,9.5,1994,羅伯特·澤米吉斯 Robert Zemeckis,一部美國近現代史。,1733112
4,這個殺手不太冷,9.4,1994,呂克·貝鬆 Luc Besson,怪蜀黍和小蘿莉不得不説的故事。,1913405
5,泰坦尼克號,9.4,1997,詹姆斯·卡梅隆 James Cameron,失去的才是永恆的。,1695453

AWK也支持三目表達式,上面語句等價於下面。

$ awk -F',' 'NR>1 {print $0 > ($3>=9 ? "douban_more_9.csv":"douban_less_9.csv")}' douban_top250.csv

同時我們可以對文件進行批量處理。

比如下面提取第二列和最後一列進行MySQL入庫。

這在數據量大的時候很管用。

比如幾萬、幾億的數據可以快速轉化為SQL語句。

### 注意 雙引號只需要斜槓轉義:\"
### 單引號除了斜槓轉義還要用''包圍起來: '\''
$ awk -F',' 'NR>1 {print "insert into `movie` (name,star) values ('\''"$2"'\'','\''"$NF"'\'');" > "movie.sql"}' douban_top250.csv

cat movie.sql|head -n 5
insert into `movie` (name,star) values ('肖申克的救贖','2304569');
insert into `movie` (name,star) values ('霸王別姬','1709820');
insert into `movie` (name,star) values ('阿甘正傳','1733112');
insert into `movie` (name,star) values ('這個殺手不太冷','1913405');
insert into `movie` (name,star) values ('泰坦尼克號','1695453');

統計Top250裏各個評分所佔數量。

$ awk -F',' 'NR>1{count[$3]++} END{for(i in count) print "豆瓣電影Top250裏評分",i,"的電影有",count[i],"個"}' douban_top250.csv
豆瓣電影Top250裏評分 9.0 的電影有 20 個
豆瓣電影Top250裏評分 9.1 的電影有 23 個
豆瓣電影Top250裏評分 9.2 的電影有 19 個
豆瓣電影Top250裏評分 9.3 的電影有 17 個
豆瓣電影Top250裏評分 9.4 的電影有 6 個
豆瓣電影Top250裏評分 9.5 的電影有 4 個
豆瓣電影Top250裏評分 9.6 的電影有 2 個
豆瓣電影Top250裏評分 9.7 的電影有 1 個
豆瓣電影Top250裏評分 8.3 的電影有 1 個
豆瓣電影Top250裏評分 8.4 的電影有 3 個
豆瓣電影Top250裏評分 8.5 的電影有 11 個
豆瓣電影Top250裏評分 8.6 的電影有 25 個
豆瓣電影Top250裏評分 8.7 的電影有 42 個
豆瓣電影Top250裏評分 8.8 的電影有 38 個
豆瓣電影Top250裏評分 8.9 的電影有 38 個

找出Top250裏拍過多個電影的導演。

$ awk -F',' 'NR>1{print $5}' douban_top250.csv|sort|uniq -c|sort -rn|head -n 5
   8 宮崎駿 Hayao Miyazaki
   7 克里斯托弗·諾蘭 Christopher Nolan
   6 史蒂文·斯皮爾伯格 Steven Spielberg
   5 王家衞 Kar Wai Wong
   5 李安 Ang Lee

找出Top250裏即拍過評分9以上 又拍過9分以下的導演。

即求 douban_less_9.csv 和 douban_more_9.csv 兩個文件的交集。

$ awk -F',' 'NR==FNR{map[$5]++} NR>FNR{if($5 in map)print $5}' douban_less_9.csv douban_more_9.csv|sort|uniq -c
   1 Chris Columbus
   2 李安 Ang Lee
   1 姜文 Wen Jiang
   1 大衞·芬奇 David Fincher
   1 羅伯·萊納 Rob Reiner
   1 劉偉強 / 麥兆輝
   1 黑澤明 Akira Kurosawa
   1 楊德昌 Edward Yang
   4 宮崎駿 Hayao Miyazaki
   2 劉鎮偉 Jeffrey Lau
   1 詹姆斯·卡梅隆 James Cameron
   2 朱塞佩·託納多雷 Giuseppe Tornatore
   3 史蒂文·斯皮爾伯格 Steven Spielberg
   1 是枝裕和 Hirokazu Koreeda
   2 弗朗西斯·福特·科波拉 Francis Ford Coppola
   3 克里斯托弗·諾蘭 Christopher Nolan

數組的key可以字符串拼接,這樣可以間接實現二維數組的邏輯。

$ awk -F',' 'NR==2,NR==5{a[$1"-"$2]=$3} END {for (i in a) print i, a[i]}' douban_top250.csv
1-肖申克的救贖 9.7
3-阿甘正傳 9.5
4-這個殺手不太冷 9.4
2-霸王別姬 9.6

數據統計的大部分需求都可以用AWK快速的實現。

比如:過濾、統計、聚合、並集、交集、差集等。

快來試試吧!!!

本文所有用到的數據集可以在奇蹟狗狗後台回覆:"awk" 進行獲取

附錄

常見的內建變量

內建變量 補充默認值 含義
NF 當前記錄的字段個數,即總共多少列
NR 讀取到的記錄數,即當前行號
FNR 當前輸入文件的記錄個數,區別於NR,NR表示整體的記錄數,FNR表示當前文件
ARGC 命令行參數的個數
ARGV 命令行參數數組
FS 指定輸入行的字段分割符
FILENAME 當前輸入文件名
OFS 指定輸出字段分割符
ORS 指定輸出的記錄分割符 默認是換行 "\n"
RS 指定輸入行的記錄分割符 默認是換行 "\n"

常見的內建函數

函數 含義
length(s) 字符串s長度
tolower(s) 把字符串轉為小寫
substr(s, p) 字符串s的子串,從位置p開始到末尾
split(s, a, fs) 把字符串s根據fs進行分割,存到數組a中
sprintf(fmt,expr-list) 跟C語言sprintf一樣,用於字符串格式化
int(x) 取x 的整數部分
sin(x) / cos(x) / sqrt(x) 正弦 / 餘弦 / 平方根
rand() 隨機數 配合 srand(x)使用 x 是 rand() 的隨機數種子
match(s,r) 正則表達式匹配,測試 s 是否包含能被 r 匹配的子串
sub(r,s) 正則表達式替換,將 $0 的第一個被r匹配的子串替換為s
gsub(r,s) 正則表達式全局替換,將 $0 中所有被r匹配的子串替換為s

關於我們

歡迎關注公眾號《奇蹟狗狗》,很開心在這裏能和你相遇~

我們會分享一些技術文章,包括但不限於遊戲技術、雲原生、ACM題解、基礎編程知識等,如果能授人以漁,榮幸之至!

我們也會做一些有温度的產品、遊戲,會陸續分享給大家,如果能博君一笑,再好不過!

產品列表:
★ WorkerHub小程序,信息均來自各個大廠員工爆料,可以查詢各個公司/部門/崗位的工作做細、工作體驗、工作評價等,供打工er找工作的時候參考,避雷卷王團隊/天坑團隊!

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.