此博文的例题来源于骏马金龙的awk课程以及awk示例的整合。一些在以往的awk学习笔记中有涉及的示例,这里就不再重复了。
处理代码注释
- # cat comment.txt
- /*AAAAAAAAAA*/ # 整行都被注释所占满。
- 1111
- 222
-
- /*aaaaaaaaa*/
- 32323
- 12341234
- 12134 /*bbbbbbbbbb*/ 132412 # 注释的左右两边有内容,需保留。
- 14534122
- /* # 跨行注释。
- cccccccccc
- */
- xxxxxx /*ddddddddddd # 跨行注释且注释的左边有内容,需保留。
- cccccccccc
- eeeeeee
- */ yyyyyyyy # 跨行注释且注释的右边有内容,需保留。
- 5642341
需要充分理解哪些是应该删除的,哪些是应该保留的。
- # cat comment.awk
- index($0,"/*"){
- if(index($0,"*/")){ # 同行包含“*/”字符串。
- # 12134 /*bbbbbbbbbb*/ 132412
- print gensub("^(.*)/\\*.*\\*/(.*)$","\\1\\2","g")
- }else{ # 同行不包含“*/”字符串。
- print gensub("^(.*)/\\*.*$","\\1","g")
- while(getline){
- if(index($0,"*/")){
- print gensub("^.*\\*/(.*)$","\\1","g")
- next # 这里不能使用break。请理解它们的区别。
- }
- }
- }
- }
- !index($0,"/*"){
- print
- }
- # awk -f comment.awk comment.txt
- 1111
- 222
-
-
- 32323
- 12341234
- 12134 132412
-
- 14534122
- xxxxxx
- yyyyyyyy
- 5642341
这个代码还有一些可以优化的点,例如去除空白行与空行。
前后段落判断
有这样的一个文件。
- # cat order.txt
- 2019-09-12 07:16:27 [-][
- 'data' => [
- 'http://192.168.100.20:2800/api/payment/i-order',
- ],
- ]
- 2019-09-12 07:16:27 [-][
- 'data' => [
- false,
- ],
- ]
- 2019-09-21 07:16:27 [-][
- 'data' => [
- 'http://192.168.100.20:2800/api/payment/i-order',
- ],
- ]
- 2019-09-21 07:16:27 [-][
- 'data' => [
- 'http://192.168.100.20:2800/api/payment/i-user',
- ],
- ]
- 2019-09-17 18:34:37 [-][
- 'data' => [
- false,
- ],
- ]
由多段构成,每一段的格式类似如下:
- YYYY-MM-DD HH:mm:SS [-][
- 'data' => [
- 'URL',
- ],
- ]
需求:找出段信息包含“false”并且它的前一段包含“i-order”,然后将符合条件的这两段信息打印出来。
思路:
- 文本信息具有规律性,修改RS使得每段信息成为一条记录。
- 需要定义一个变量来保存前一段信息。
- 当前段信息和前一段信息需要同时满足条件。
- # cat order.awk
- BEGIN{
- ORS=RS="]\n"
- }
- {
- if($0~/false/&&prev~/i-order/){ # 只有第一条记录的$0会和prev相同。如果第一条记录同时包含了“false”和“i-order”,那么就要另作考虑了。
- print prev
- print $0
- }
- prev=$0
- }
- # awk -f order.awk order.txt
- 2019-09-12 07:16:27 [-][
- 'data' => [
- 'http://192.168.100.20:2800/api/payment/i-order',
- ],
- ]
- 2019-09-12 07:16:27 [-][
- 'data' => [
- false,
- ],
- ]
行列转换
示例一
这道题我个人认为是比较经典的一道题目,尤其是进阶版的考察了awk的许多方面。
首先我们来看基础版,也就是作者的原版。
- # cat RowColumnConvert.txt
- ID name gender age email
- 1 Bob male 28 qq.com
- 2 Alice female 20 163.com
- 3 Tony male 18 gmail.com
- 4 Kevin female 30 xyz.com
期望将行转换成列。
- ID 1 2 3 4
- name Bob Alice Tony Kevin
- gender male female male female
- age 28 20 18 30
- email qq.com 163.com gmail.com xyz.com
原作者给出的答案。
- # cat RowColumnConvert.awk
- {
- for(i=1;i<=NF;i++){
- if(typeof(arr[i])=="unassigned"){
- arr[i]=$i
- }else{
- arr[i]=arr[i]"\t"$i
- }
- }
- }
- END{
- for(i=1;i<=NF;i++){
- print arr[i]
- }
- }
这种使用字符串连接再在其中加入一个制表符来构建的方式,如果某些记录的长度过长或者过短,就会导致排版的不统一。
在该示例中则是原第5行第4列“gmail.com”长度过长导致的。

这个代码要求每一行同字段之间的长度不可以太长。
因此我们来看一下进阶版,要求行列转换以后要对齐。
- 首先需要先将原始数据保存起来,然后再输出。原始数据由第N行第N列以及其对应的具体值来表述,例如“第3行第3列是female”,那么需要存储的信息就有3个,就可以使用二维数组。
- 使用变量i表示原始数据的行,变量j表示原始数据的列。在脑中要有这样的思路,不然很容易出错。
- 原文件行数和列数一致,容易造成误导,最好修改一下,使它们不一致。
- 对齐的思路是我们去计算应该填充多少空格字符。
- # cat RowColumnConvert2.awk
- {
- for(j=1;j<=NF;j++){
- arr[NR,j]=$j
- len[j]=length($j)
- maxLength[NR]=len[j]>maxLength[NR]?len[j]:maxLength[NR]
- }
- }
- func cat(count ,str,x){ # 这里的“局部变量”的定义很重要,尤其是如果这里使用了同名变量i或者j的情况下!
- for(x=1;x<=count;x++){
- str=str" "
- }
- return str
- }
- END{
- for(j=1;j<=NF;j++){
- for(i=1;i<=NR;i++){
- if(typeof(brr[j])=="unassigned"){
- brr[j]=arr[i,j]""cat(maxLength[i]-length(arr[i,j]))" "
- }else{
- brr[j]=brr[j]""arr[i,j]""cat(maxLength[i]-length(arr[i,j]))" "
- }
- }
- print brr[j]
- }
- }
- # awk -f RowColumnConvert2.awk RowColumnConvert.txt
- ID 1 2 3 4 5
- name Bob Alice Tony Kevin Tom
- gender male female male female male
- age 28 20 18 30 25
- email qq.com 163.com gmail.com xyz.com alibaba.com
示例二
期望转换成:
- # cat RowColumnConvert3.awk
- {
- for(i=1;i<=NF;i++){
- if(typeof(arr[i])=="unassigned"){
- arr[i]=$i
- }else{
- arr[i]=arr[i]" "$i
- }
- }
- }
- END{
- for(i=1;i<=NF;i++){
- print arr[i]
- }
- }
- # awk -f RowColumnConvert3.awk test.txt
- name alice ryan
- age 21 30
示例三
- # cat test.txt
- 74683 1001
- 74683 1002
- 74683 1011
- 74684 1000
- 74684 1001
- 74684 1002
- 74685 1001
- 74685 1011
- 74686 1000
- 100085 1000
- 100085 1001
期望输出:
- 74683 1001 1002 1011
- 74684 1000 1001 1002
- 74685 1001 1011
- 74686 1000
- 100085 1000 1001
- # cat RowColumnConvert4.awk
- {
- if(!$1 in arr){
- arr[$1]=$2
- }else{
- arr[$1]=arr[$1]" "$2
- }
- }
- END{
- for(i in arr){
- print i,arr[i]
- }
- }
- # awk -f RowColumnConvert4.awk test.txt
- 74683 1001 1002 1011
- 74684 1000 1001 1002
- 74685 1001 1011
- 74686 1000
- 100085 1000 1001
格式化空白字符
主要涉及awk对于$N进行修改时会基于OFS来重建$0。在【字段与记录的重建】中我们已经提到过。
- # cat chaos.txt
- aaa bb cccc
- dd ee ff gg
- hhhhh i jjjj
- # awk 'BEGIN{OFS="\t"}{$1=$1;print}' chaos.txt
- aaa bb cccc
- dd ee ff gg
- hhhhh i jjjj
在Linux中是对齐的,不晓得是不是博客园【插入代码】显示的问题。

筛选IP地址
目标是从ifconfig的输出结果中筛选出IPv4地址。这题我们以前就做过,具体的解题思路详见读取文件中的【数据筛选示例】,这里直接给答案。
- ifconfig | awk '/inet /&&!/127.0.0.1/{print $2}'
- ifconfig | awk 'BEGIN{RS=""}!/^lo/{print $6}'
- ifconfig | awk 'BEGIN{RS="";FS="\n"}!/^lo/{FS=" ";$0=$2;print $2;FS="\n"}'
读取配置文件中的某段
这里我们以yum源的配置文件为例。我们过滤掉注释和空行。
- # grep -vE "^#|^$" /etc/yum.repos.d/CentOS-Base.repo
- ... ...
- [extras]
- name=CentOS-$releasever - Extras
- mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras&infra=$infra
- gpgcheck=1
- gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
- ... ...
期望仅取出某一段数据,例如[extras]段。
思路一:
- 配置文件具备规律性,将中括号作为记录分隔符。
- 基于上面那点再修修补补即可取到想要的信息。
- # grep -vE "^#|^$" /etc/yum.repos.d/CentOS-Base.repo | awk 'BEGIN{RS="[";ORS=""}/^extras/{print "["$0}'
- [extras]
- name=CentOS-$releasever - Extras
- mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras&infra=$infra
- gpgcheck=1
- gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
思路二:
- 先找extras那行,找到以后输出。
- 随后循环getline并打印,直到遇到下一个配置段“[.+]”。
- # cat extract.awk
- index($0,"[extras]"){
- print
- while((getline)>0){
- if($0~/\[.+\]/){
- break
- }
- print
- }
- }
- # grep -vE "^#|^$" /etc/yum.repos.d/CentOS-Base.repo | awk -f extract.awk
- [extras]
- name=CentOS-$releasever - Extras
- mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras&infra=$infra
- gpgcheck=1
- gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
根据$0中的部分信息进行去重
首先来看示例文件。
- # cat partDuplicate.txt
- 2019-01-13_12:00_index?uid=123
- 2019-01-13_13:00_index?uid=123
- 2019-01-13_14:00_index?uid=333
- 2019-01-13_15:00_index?uid=9710
- 2019-01-14_12:00_index?uid=123
- 2019-01-14_13:00_index?uid=123
- 2019-01-15_14:00_index?uid=333
- 2019-01-16_15:00_index?uid=9710
如果问号后面的“uid=xxx”相同,我们就认为是重复的数据,并且将其去除。
输出的时候,我们要保证原本的数据出现的顺序,因此就不应存入数组并进行无序遍历了。
思路在数组的实战中我们就有接触过了。
思路一:
以问号作为FS,将$2作为数组索引,每次awk内部循环对arr[$2]进行自增,第一次出现的数据arr[$2]的值就为1,仅针对第一次出现的数据进行输出即可。
- # awk 'BEGIN{FS="?"}{arr[$2]++;if(arr[$2]==1){print}}' partDuplicate.txt
- 2019-01-13_12:00_index?uid=123
- 2019-01-13_14:00_index?uid=333
- 2019-01-13_15:00_index?uid=9710
思路二:
我们可以将“!arr[$2]++”拿来做pattern,第一次出现数据时返回值为1,往后的返回值均是0。
action部分只需要输出,并且以下三者等价:
- PAT{print $0}
- PAT{print}
- PAT
关于pattern和action的省略情况,详见这里。因此我们就只需要pattern即可。
- # awk 'BEGIN{FS="?"}!arr[$2]++' partDuplicate.txt
- 2019-01-13_12:00_index?uid=123
- 2019-01-13_14:00_index?uid=333
- 2019-01-13_15:00_index?uid=9710
次数统计
示例文件:
- # cat test.txt
- portmapper
- portmapper
- portmapper
- portmapper
- portmapper
- portmapper
- status
- status
- mountd
- mountd
- mountd
- mountd
- mountd
- mountd
- nfs
- nfs
- nfs_acl
- nfs
- nfs
- nfs_acl
- nlockmgr
- nlockmgr
- nlockmgr
- nlockmgr
- nlockmgr
- # awk '{arr[$0]++}END{for(i in arr){print i"-->"arr[i]}}' test.txt
- nfs-->4
- status-->2
- nlockmgr-->5
- portmapper-->6
- nfs_acl-->2
- mountd-->6
统计TCP连接状态数量
详见数组的实战部分。
根据http状态码统计日志中各IP的出现次数
需求:统计web日志中,http状态码非200的客户端IP的出现次数,按照降序的方式统计出前10行。
日志文件放百度网盘了,提取码是jtlg。
- 111.202.100.141 - - [2019-11-07T03:11:02+08:00] "GET /robots.txt HTTP/1.1" 301 169 "-" "Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)" "-"
- # awk '$8!=200{arr[$1]++} END{PROCINFO["sorted_in"]="@val_num_desc";for(i in arr){if(cnt++==10){break}print arr[i]"-->"i}}' access.log
- 896-->60.21.253.82
- 75-->216.83.59.82
- 21-->211.95.50.7
- 21-->61.241.50.63
- 20-->59.36.132.240
- 18-->182.254.52.17
- 16-->50.7.235.2
- 15-->101.89.19.140
- 15-->94.102.50.96
- 13-->198.108.67.80
统计独立IP
- # cat independence.txt
- a.com.cn|202.109.134.23|2015-11-20 20:34:43|guest
- b.com.cn|202.109.134.23|2015-11-20 20:34:48|guest
- c.com.cn|202.109.134.24|2015-11-20 20:34:48|guest
- a.com.cn|202.109.134.23|2015-11-20 20:34:43|guest
- a.com.cn|202.109.134.24|2015-11-20 20:34:43|guest
- b.com.cn|202.109.134.25|2015-11-20 20:34:48|guest
从该文件中统计每个域名及其对应的独立IP数。
例如,a.com.cn的行有3条,但是独立IP只有2个。因此需要记录的信息就是:
将所有的域名及其独立IP的数量统计后输出到“域名.txt”格式的文件中。
- # awk 'BEGIN{FS="|"} !arr[$1,$2]++{brr[$1]++} END{for(i in brr){print i,brr[i]>i".txt"}}' independence.txt
- # cat a.com.cn.txt
- a.com.cn 2
- # cat b.com.cn.txt
- b.com.cn 2
- # cat c.com.cn.txt
- c.com.cn 1
两个文件的处理
存在两个文件file1.txt和file2.txt:
- # cat file1.txt
- 50.481 64.634 40.573 1.00 0.00
- 51.877 65.004 40.226 1.00 0.00
- 52.258 64.681 39.113 1.00 0.00
- 52.418 65.846 40.925 1.00 0.00
- 49.515 65.641 40.554 1.00 0.00
- 49.802 66.666 40.358 1.00 0.00
- 48.176 65.344 40.766 1.00 0.00
- 47.428 66.127 40.732 1.00 0.00
- 51.087 62.165 40.940 1.00 0.00
- 52.289 62.334 40.897 1.00 0.00
- # cat file2.txt
- 48.420 62.001 41.252 1.00 0.00
- 45.555 61.598 41.361 1.00 0.00
- 45.815 61.402 40.325 1.00 0.00
- 44.873 60.641 42.111 1.00 0.00
- 44.617 59.688 41.648 1.00 0.00
- 44.500 60.911 43.433 1.00 0.00
- 43.691 59.887 44.228 1.00 0.00
- 43.980 58.629 43.859 1.00 0.00
- 42.372 60.069 44.032 1.00 0.00
- 43.914 59.977 45.551 1.00 0.00
需求:替换file2.txt的第5列的值为file2.txt的第1列减去file1.txt的第1列的值。
方法一
- # cat twoFile1.awk
- {
- num1=$1
- if((getline < "file2.txt")>0){
- $5=$1-num1
- print $0
- }
- }
- # awk -f twoFile1.awk file1.txt
- 48.420 62.001 41.252 1.00 -2.061
- 45.555 61.598 41.361 1.00 -6.322
- 45.815 61.402 40.325 1.00 -6.443
- 44.873 60.641 42.111 1.00 -7.545
- 44.617 59.688 41.648 1.00 -4.898
- 44.500 60.911 43.433 1.00 -5.302
- 43.691 59.887 44.228 1.00 -4.485
- 43.980 58.629 43.859 1.00 -3.448
- 42.372 60.069 44.032 1.00 -8.715
- 43.914 59.977 45.551 1.00 -8.375
方法二
我们期望将file1.txt和file2.txt都直接作为命令的参数。形如:
- awk '...rule...' file1.txt file2.txt
- # cat twoFile2.awk
- NR==FNR{ # 如果NR和FNR相等,那么就表示awk在处理的文件是第一个文件
- arr[FNR]=$1
- }
- NR!=FNR{
- $5=$1-arr[FNR]
- print $0
- }
- # awk -f twoFile2.awk file1.txt file2.txt
- 48.420 62.001 41.252 1.00 -2.061
- 45.555 61.598 41.361 1.00 -6.322
- 45.815 61.402 40.325 1.00 -6.443
- 44.873 60.641 42.111 1.00 -7.545
- 44.617 59.688 41.648 1.00 -4.898
- 44.500 60.911 43.433 1.00 -5.302
- 43.691 59.887 44.228 1.00 -4.485
- 43.980 58.629 43.859 1.00 -3.448
- 42.372 60.069 44.032 1.00 -8.715
- 43.914 59.977 45.551 1.00 -8.375