乐趣区

关于后端: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 找工作的时候参考,避雷卷王团队 / 天坑团队!

退出移动版