原来awk真是神器啊
原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。
简介
刚开始入门awk时,觉得awk很简单,像是一个玩具,根本无法应用到工作之中,但随着对awk的了解不断加深,就会越发觉得这玩意的强大,大佬们称其为上古神器,绝不是空穴来风。
这也可以说明,一些热门的技术知识点,如果你觉得它不过如此,那绝对是你对它的掌握不够深入,而不是它没啥用。
基本语法
awk基本语法如下:
awk 'BEGIN{//your code} pattern1{//your code} pattern2{//your code} END{//your code}'
- BEGIN部分的代码,最先执行
- 然后循环从管道中读取的每行文本,如果匹配pattern1,则执行pattern1的代码,匹配pattern2,则执行pattern2中的代码
- 最后,执行END部分的代码
如下所示,分别求奇数行与偶数行的和:
$ seq 1 5
1
2
3
4
5
$ seq 1 5|awk 'BEGIN{print "odd","even"} NR%2==1{odd+=$0} NR%2==0{even+=$0} END{print odd,even}'
odd even
9 6
seq 1 5
用来生成1到5的数字。- awk先执行BEGIN块,打印标题。
- 然后第1行尝试匹配
NR%2==1
这个pattern,其中NR为awk的内置变量,表示行号,$0
为awk读到的当前行数据,显然匹配NR%2==1
,则执行里面的代码。 - 然后第1行尝试匹配
NR%2==0
这个pattern,显然不匹配。 - 然后第2行、第3行...,一直到最后一行,都执行上面两步。
- 最后执行END块,将前面求和的变量打印出来,其中
9=1+3+5
,6=2+4
。
这个程序还可以如下这样写:
seq 1 5|awk 'BEGIN{print "odd","even"} {if(NR%2==1){odd+=$0}else{even+=$0}} END{print odd,even}'
这里使用了if
语句,实际上awk的程序语法与C是非常类似的,所以awk也有else
,while
,for
,break
,continue
,exit
。
另外,可以看到,awk程序在处理时,默认是一行一行处理的,注意我这里说的是默认,并不代表awk只能一行一行处理数据,接下来看看awk的分列功能,可通过-F
选项提供,如下:
$ paste <(seq 1 5) <(seq 6 10) -d,
1,6
2,7
3,8
4,9
5,10
$ paste <(seq 1 5) <(seq 6 10) -d, |awk -F, '{printf "%s\t%s\n",$1,$2}'
1 6
2 7
3 8
4 9
5 10
这个例子用-F
指定了,
,这样awk会自动将读取到的每行,使用,
分列,拆分后的结果保存在$1,$2...
中,另外,你也可以使用$NF,$(NF-1)
来引用最后两列的值,不指定-F时,awk默认使用空白字符分列。
注意这里面的printf "%s\t%s\n",$1,$2
,printf是一个格式化打印函数,其实也可以写成printf("%s\t%s\n", $1, $2)
,只不过awk中函数调用可以省略括号。
上面已经提到了NR这个内置变量,awk还有如下内置变量
内置变量 | 作用 |
---|---|
FS | 与-F功能类似,用来分列的,不过FS可以是正则表达式,默认是空白字符 |
OFS | 与FS对应,指定print函数输出时的列分隔符,默认空格 |
RS | 记录分隔符,默认记录分隔符是\n |
ORS | 与RS对应,指定print函数输出时的记录分隔符,默认\n |
用2个例子体会一下:
$ echo -n '1,2,3|4,5,6|7,8,9'|awk 'BEGIN{RS="|";FS=","} {print $1,$2,$3}'
1 2 3
4 5 6
7 8 9
$ echo -n '1,2,3|4,5,6|7,8,9'|awk 'BEGIN{RS="|";FS=",";ORS=",";OFS="|"} {print $1,$2,$3}'
1|2|3,4|5|6,7|8|9,
总结:awk数据读取模式,总是以RS为记录分隔符,一条一条记录的读取,然后每条记录按FS拆分为字段。
再看看这个例子:
$ seq 1 5|awk '/^[1-4]/ && !/^[3-4]/'
1
2
$ seq 1 5|awk '$0 ~ /^[1-4]/ && $0 !~ /^[3-4]/{print}'
1
2
$ seq 1 5|awk '$0 ~ /^[1-4]/ && $0 !~ /^[3-4]/{print $0}'
1
2
可以看到:
- awk中pattern部分可以直接使用正则表达式,而且可以使用
&&
,||
,!
这样的逻辑运算符. - 如果正则表达式没有指定匹配变量,默认对$0执行匹配,所以
awk '/regex/'
直接就可以等效于grep -E 'regex'
. - 另外pattern后面的代码部分如果省略的话,默认打印$0.
- print函数如果没有指定参数,也默认打印$0.
- 另外,一定注意awk中的正则表达式不支持
\d
,匹配数字请使用[0-9]
,因为linux中正则语法分BRE,ERE,PCRE,而awk支持的是ERE.
到这里,awk基本语法就差不多讲完了,接下来会介绍一些常用的场景。
查找与提取
示例数据如下,也是用awk生成的:
$ seq 1 10|awk '{printf "id=%s,name=person%s,age=%d,sex=%d\n",$0,$0,$0/3+15,$0/6}'|tee person.txt
id=1,name=person1,age=15,sex=0
id=2,name=person2,age=15,sex=0
id=3,name=person3,age=16,sex=0
id=4,name=person4,age=16,sex=0
id=5,name=person5,age=16,sex=0
id=6,name=person6,age=17,sex=1
id=7,name=person7,age=17,sex=1
id=8,name=person8,age=17,sex=1
id=9,name=person9,age=18,sex=1
id=10,name=person10,age=18,sex=1
然后用awk模拟select id,name,age from person where age > 15 and age < 18 limit 4
这样SQL的逻辑,如下:
$ cat person.txt |awk 'match($0, /^id=(\w+),name=(\w+),age=(\w+)/, a) && a[3]>15 && a[3]<18 { print a[1],a[2],a[3]; if(++limit >= 4) exit 0}'
3 person3 16
4 person4 16
5 person5 16
6 person6 17
- 首先使用match函数以及正则表达式的捕获组功能,将
id,name,age
的值提取到a[1],a[2],a[3]
中. - 然后
a[3]>15 && a[3]<18
即类似SQL中age > 15 and age < 18
的功能. - 然后打印
a[1],a[2],a[3]
,类似SQL中select id,name,age
的功能. - 最后,如果打印条数到达4条,退出程序,即类似
limit 4
的功能.
简单统计分析
awk可以做一些简单的统计分析任务,还是以SQL为例。
如select age,sex,count(*) num, group_concat(id) ids from person where age > 15 and age < 18 group by age,sex
这样的统计SQL,用awk实现如下:
$ cat person.txt |awk '
BEGIN{
printf "age\tsex\tnum\tids\n"
}
match($0, /^id=(\w+),name=(\w+),age=(\w+),sex=(\w+)/, a) && a[3]>15 && a[3]<18 {
s[a[3],a[4]]["num"]++;
s[a[3],a[4]]["ids"] = (s[a[3],a[4]]["ids"] ? s[a[3],a[4]]["ids"] "," a[1] : a[1])
}
END{
for(key in s){
split(key, k, SUBSEP);
age=k[1];
sex=k[2];
printf "%s\t%s\t%s\t%s\n",age,sex,s[age,sex]["num"],s[age,sex]["ids"]
}
}'
age sex num ids
17 1 3 6,7,8
16 0 3 3,4,5
awk代码稍微有点长了,但逻辑还是很清晰的。
- BEGIN中打印标题行.
- match获取出id,name,age,sex,并过滤age>15且age<18的数据,然后将统计结果累计到s这个关联数组中。你可以把s这个关联数组想象中map,然后只是有两级key而已。(注意在awk中,拼接字符串使用空格即可,并不像java中使用+号).
- 最后END块中,遍历s这个关联数组,注意,类似
s[a[3],a[4]]
这样,在awk中是一个key,awk会使用SUBSEP这个变量将a[3]
,a[4]
拼接起来,需要split(key, k, SUBSEP)
,将key按SUBSEP拆分到k中,SUBSEP默认是\034
文件分隔符.
处理csv
csv是以一行为一条记录,以,
号分隔为列的数据格式,awk天然适合处理csv这样的数据,但在列值中本身存在,
号时,只使用-F
就不行了,这时可以使用FPAT
,如下:
$ echo 'aa,"bb,cc",dd'|gawk 'BEGIN { FPAT = "[^,]+|\"[^\"]+\"" } { print $3 }'
dd
FPAT变量用于提取字段值,这里指定为正则表达式,只不过这个正则表达式可以匹配类似aa
或"bb,cc"
这种数据.
按段拆分记录
awk可以按段拆分记录,什么是段呢,看一下ifconfig的输出,如下:
$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.73.125 netmask 255.255.240.0 broadcast 172.17.79.255
inet6 fe80::215:5dff:fe4c:c155 prefixlen 64 scopeid 0x20<link>
ether 00:15:5d:4c:c1:55 txqueuelen 1000 (Ethernet)
RX packets 35008 bytes 5829277 (5.8 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 4435 bytes 7152614 (7.1 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 82 bytes 4100 (4.1 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 82 bytes 4100 (4.1 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
ifconfig的输出中,eth0与lo的数据使用空行间隔开了,像这种,用空行间隔的内容,其中eth0部分是一个段,lo部分也是一个段,而在awk中,RS=""
即表示按段拆分记录,所以获取eth0网卡ip地址的方法是:
$ ifconfig|awk -v RS="" '/^eth0/{print $6}'
172.17.73.125
这里用-v RS=""
来设置RS变量,与在BEGIN中效果是一样的,-v name=var
是shell向awk内部传递变量的一种方法。
另外,java中jstack获取的线程栈的内容,也是以空行分段输出的,用awk来处理也很简单,比如,我们只看目前与mysql有关的线程栈,如下:
$ jstack `pgrep java`|awk -v RS="" '/mysql/'
"Abandoned connection cleanup thread" #18 daemon prio=5 os_prio=0 tid=0x00007fbb893d0000 nid=0xd75 in Object.wait() [0x00007fbb586ad000]
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x00000000e160ee38> (a java.lang.ref.ReferenceQueue$Lock)
at com.mysql.jdbc.AbandonedConnectionCleanupThread.run(AbandonedConnectionCleanupThread.java:40)
到这里,我们不难想到,用awk来搜索异常日志,其实比grep更方便,比如java中如下的错误日志:
$ cat app_demo.log
[2020-12-23 20:12:51] [app_demo] [172.29.128.1] [INFO] [RMI TCP Connection(7)-172.29.128.1] {"logger":"o.a.catalina.core.ContainerBase.[Tomcat].[localhost].[/]","msg":"Initializing Spring DispatcherServlet 'dispatcherServlet'"}
[2020-12-23 20:12:51] [app_demo] [172.29.128.1] [INFO] [RMI TCP Connection(7)-172.29.128.1] {"logger":"org.springframework.web.servlet.DispatcherServlet","msg":"Initializing Servlet 'dispatcherServlet'"}
[2020-12-23 20:12:51] [app_demo] [172.29.128.1] [INFO] [RMI TCP Connection(7)-172.29.128.1] {"logger":"org.springframework.web.servlet.DispatcherServlet","msg":"Completed initialization in 26 ms"}
[2020-12-23 20:15:00] [app_demo] [172.29.128.1] [ERROR] [redisson-netty-1-6] {"logger":"org.redisson.client.handler.CommandsQueue","msg":"Exception occured. Channel: [id: 0x43577278, L:/127.0.0.1:61888 - R:localhost/127.0.0.1:22122]
java.io.IOException: 远程主机强迫关闭了一个现有的连接。
at sun.nio.ch.SocketDispatcher.read0(Native Method)
at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:43)
at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
at sun.nio.ch.IOUtil.read(IOUtil.java:192)
at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:380)
at io.netty.buffer.PooledUnsafeDirectByteBuf.setBytes(PooledUnsafeDirectByteBuf.java:288)
at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132)
at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:347)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:148)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:648)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:583)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:500)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:462)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:897)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:748)"}
# 使用grep直接搜索IOException,效果如下,看不到前后的调用栈了
$ cat app_demo.log |grep 'IOException'
java.io.IOException: 远程主机强迫关闭了一个现有的连接。
# grep可以使用-B -C指定匹配内容前面显示几行,后面显示几行,如grep -B 2 -C 10
# 效果如下,由于不知道线程栈有多深,-C有时会设置小了,有时又会设置大了
$ cat app_demo.log |grep -B2 -C10 'IOException'
[2020-12-23 20:12:51] [app_demo] [172.29.128.1] [INFO] [RMI TCP Connection(7)-172.29.128.1] {"logger":"org.springframework.web.servlet.DispatcherServlet","msg":"Completed initialization in 26 ms"}
[2020-12-23 20:15:00] [app_demo] [172.29.128.1] [ERROR] [redisson-netty-1-6] {"logger":"org.redisson.client.handler.CommandsQueue","msg":"Exception occured. Channel: [id: 0x43577278, L:/127.0.0.1:61888 - R:localhost/127.0.0.1:22122]
java.io.IOException: 远程主机强迫关闭了一个现有的连接。
at sun.nio.ch.SocketDispatcher.read0(Native Method)
at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:43)
at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
at sun.nio.ch.IOUtil.read(IOUtil.java:192)
at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:380)
at io.netty.buffer.PooledUnsafeDirectByteBuf.setBytes(PooledUnsafeDirectByteBuf.java:288)
at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132)
at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:347)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:148)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:648)"
# 使用awk,RS='\n\\S',效果如下,简直完美!
$ cat app_demo.log |awk -v RS='\n\\S' '/IOException/'
2020-12-23 20:15:00] [app_demo] [172.29.128.1] [ERROR] [redisson-netty-1-6] {"logger":"org.redisson.client.handler.CommandsQueue","msg":"Exception occured. Channel: [id: 0x43577278, L:/127.0.0.1:61888 - R:localhost/127.0.0.1:22122]
java.io.IOException: 远程主机强迫关闭了一个现有的连接。
at sun.nio.ch.SocketDispatcher.read0(Native Method)
at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:43)
at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
at sun.nio.ch.IOUtil.read(IOUtil.java:192)
at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:380)
at io.netty.buffer.PooledUnsafeDirectByteBuf.setBytes(PooledUnsafeDirectByteBuf.java:288)
at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132)
at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:347)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:148)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:648)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:583)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:500)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:462)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:897)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:748)"}
这里的RS='\n\\S'
是一个正则表达式,记录分隔符为换行符后面带一个非空白符。
最后,指定RS='^$'
可以将数据一次性的读取到$0
中,这也是一个常用的小技巧,因为^$
显然无法匹配任何字符,这样awk就会将所有数据读取到一条记录中了。
如下,将多行数据用,
拼接为一行数据:
$ seq 1 10|awk -v RS='^$' '{gsub(/\n/, "," , $0);print $0}'
1,2,3,4,5,6,7,8,9,10,
范围匹配
awk中另一个比grep强的地方,就是awk可以使用范围过滤数据,而grep只能使用正则,比如我们要搜索2021-01-04 23:33:40
到2021-01-04 23:34:16
的日志:
# 搜索2021-01-04 23:33:40到2021-01-04 23:34:16的日志,前提是这两个时间在日志中都存在,因为awk是在遇到2021-01-04 23:33:40后,开启打印,直到遇到2021-01-04 23:34:16又关闭打印
cat app.log|awk '/2021-01-04 23:33:40/,/2021-01-04 23:34:16/'
# 另一种更有效的方式
cat app.log|awk 'match($0,/^\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})\]/,a){
if(a[1]>="2021-01-07 12:26:43")
print $0;
if(a[1]>"2021-01-07 12:26:56") exit
}'
多文件join处理
awk还可以实现类似SQL中的join处理,求交集或差集,如下:
$ cat user.txt
1 zhangsan
2 lisi
3 wangwu
4 pangliu
$ cat score.txt
1 86
2 57
3 92
# 类似 select a.id,a.name,b.score from user a left join score b on a.id=b.id
# 这里FNR是当前文件中的行号,而NR一直是递增的,所以对于第一个score.txt,NR==FNR成立,第二个user.txt,NR!=FNR成立
$ awk 'NR==FNR{s[$1]=$2} NR!=FNR{print $1,$2,s[$1]}' score.txt user.txt
1 zhangsan 86
2 lisi 57
3 wangwu 92
4 pangliu
# 当然,也可以直接使用FILENAME内置变量,如下
$ awk 'FILENAME=="score.txt"{s[$1]=$2} FILENAME=="user.txt"{print $1,$2,s[$1]}' score.txt user.txt
# 求差集,打印user.txt不在score.txt中的行
$ awk 'FILENAME=="score.txt"{s[$1]=$2} FILENAME=="user.txt" && !($1 in s){print $0}' score.txt user.txt
4 pangliu
awk常用函数
函数名 | 说明 | 示例 |
---|---|---|
sub | 替换一次 | sub(/,/,"|",$0) |
gsub | 替换所有 | gsub(/,/,"|",$0) |
match | 匹配,捕获内容在a数组中 | match($0,/id=(\w+)/,a) |
split | 拆分,拆分内容在a数组中 | split($0,a,/,/) |
index | 查找字符串,返回查找到的位置,从1开始 | i=index($0,"hi") |
substr | 截取子串 | substr($0,1,i) 或substr($0,i) |
tolower | 转小写 | tolower($0) |
toupper | 转大写 | toupper($0) |
srand,rand | 生成随机数 | BEGIN{srand();printf "%d",rand()*10} |
替换其它文本处理命令
grep
命令 | 说明 | awk实现 |
---|---|---|
grep 'regex' |
过滤记录 | awk '/regex/' |
sed
命令 | 说明 | awk实现 |
---|---|---|
sed -n '/regex/ p' |
过滤记录 | awk '/regex/' |
sed -n '1,5 p' |
显示前5行 | awk 'NR<=5' |
sed '/1~2/ s/hello/hi/g' |
奇数行所有hello替换为hi | awk 'NR%2==1{gsub(/hello/,"hi",$0);print $0}' |
sed '/regex/ d' |
删除匹配行 | awk '!/regex/' |
sed '1 i\id,name' |
第1行插入一行标题 | awk '{if(NR==1) print "id,name"; print $0}' |
sed '1 a\id,name' |
第1行后面添加一行 | awk '{print $0; if(NR==1) print "id,name"}' |
sed '1 c\id,name' |
修改第1行整行内容 | awk '{if(NR==1){print "id,name"}else{print $0}}' |
可以发现,sed其实是awk程序在某些场景的固化,因为awk程序类似awk 'pattern{}'
,而sed程序类似sed 'pattern action'
,action就是p,s,d,i,a,c
这些动作,而这些动作对应awk固化在{}
中的代码。
然后grep是进一步的场景固化,它只支持正则过滤。
tr
命令 | 说明 | awk实现 |
---|---|---|
tr -d '\n' |
删除换行符 | awk -v RS='^$' '{gsub(/\n/, "" , $0);print $0}' |
tr -s ' ' |
压缩多个空格为一个,awk的这个实现有点hack | awk '{$1=$1;print $0}' |
tr [a-z] [A-Z] |
转大写 | awk '{print toupper($0)}' |
cut
命令 | 说明 | awk实现 |
---|---|---|
cut -d, -f2 |
用, 拆分取第2个字段 |
awk -F, '{print $2}' |
head
命令 | 说明 | awk实现 |
---|---|---|
head -n10 |
取前10行 | awk '{print $0;if(++n >= 10) exit 0}' |
tail
命令 | 说明 | awk实现 |
---|---|---|
tail -n10 |
取倒数10行 | 这个直接用awk实现有点长,没必要,可以用tac辅助 `tac test.log |
wc
命令 | 说明 | awk实现 |
---|---|---|
wc -l |
统计文件行数 | awk 'END{print NR}' |
uniq
命令 | 说明 | awk实现 |
---|---|---|
uniq -c |
分组计数 | awk '{s[$0]++} END{for(k in s){print s[k],k}}' |
总结
awk其本身就是为文本处理而生的,不太复杂的临时性的文本处理分析,第一个想到的就应该是它!
后面,我再总结一下shell本文处理的常见技巧,作为本篇的补充。
往期内容
不容易自己琢磨出来的正则表达式用法
好用的parallel命令
还在胡乱设置连接空闲时间?
常用网络命令总结
使用socat批量操作多台机器