前言
之前針對WorkerHub小程序做的數據分析文章 互聯網卷王花落誰家? 收到了一些小伙伴的回復,點名要學習數據分析,其實我也是一知半解,想著來寫幾篇文章簡單聊下我分析的過程。
首先是數據清洗和統計了,這塊我并沒有用諸如Python之類的腳本語言,雖然Python提供了很多強大的數據分析庫如Pandas、Numpy等,但是畢竟要麻煩一些,還要寫個腳本,裝一堆庫(PS:其實就是懶)。
我用的是一個老的Unix工具 AWK,雖然歷史比較久遠,但是它簡潔和豐富的功能可以稱之為神器,同時也是后臺同學必須要掌握的一個工具,畢竟通過日志緊急定位線上問題的時候,你不能跟老板說:等等我先寫個腳本,那老板原地暴斃了。
應用場景
AWK是1977年貝爾實驗室的三個兄弟( Alfred Aho、Peter Weinberger、 Brian Kernighan )搞出來的文本分析工具,這三個哥們的首字母拼起來就是AWK的名字了。
AWK處理文本就像其他語言處理數值一樣方便,所以經常被應用在文本處理領域。
比如日志分析、數據清洗、文本過濾、數據統計等。
同時AWK也是一門編程語言,不過它的命令行用法就可以滿足大多數的應用場景。
我們通??梢允褂靡恍蠥WK命令完成一個腳本的任務?。。?/p>
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 ?。。?/strong>
基本用法
一個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列,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/gawk/manual/html_node/Control-Letters.html 。
模式過濾
上面介紹了動作的使用,動作通常用來輸出展示。
模式用來過濾我們想要的記錄。
如下篩選(行號>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 |