awk
导言
很多刚接触 awk,sed 等命令时, 看到帮助文档一堆参数, 一堆符号感觉有点慌, 我刚开始学习时也出现过这样的问题, 这篇文章从我们工作遇到的问题出发, 由浅入深, 重点在于阐述其工作原理和最常用的用法(覆盖我们工作 80% 的就很满意了), 作为通读性强的文章希望能利用上下班的时间就能看懂, 树立一个 awk 能帮我们解决哪些问题的意识. 当然高级用法可以基本本篇给的思路去摸索, 另外会不定期的更新使用的例子.
简介
awk 工作流程和原理
awk 使用例子积累
面向
有用过有点迷糊想系统学习的朋友, 完全没用过的朋友
修改:
awk 是一种编程语言, 用于在 linux/unix 下对文本和数据进行处理. 数据可以来自标准输入(stdin), 一个或多个文件, 或其它命令的输出. 它支持用户自定义函数和动态正则表达式等先进功能, 它在命令行中使用, 但更多是作为脚本来使用.
awk 语法说明
语法形式
- awk [options] 'script' var=value file(s)
- awk [options] -f scriptfile var=value file(s)
常用命令选项
-F fs fs 指定输入分隔符, fs 可以是字符串或正则表达式, 如 - F:
-v var=value 赋值一个用户定义变量, 将外部变量传递给 awk
-f scripfile 从脚本文件中读取 awk 命令
语法结构
awk 是由 pattern 和 action 组成, pattern 表示 AWK 在数据中查找的内容, 而 action 是在找到匹配内容时所执行的一系列命令.
awk '{pattern + action}' {filenames}
pattern 可以是如下几种或者什么都没有(全部匹配):
/ 正则表达式 /: 使用通配符的扩展集.
关系表达式: 使用运算符进行操作, 可以是字符串或数字的比较测试.
模式匹配表达式: 用运算符~(匹配)和~!(不匹配).
BEGIN 语句块, pattern 语句块, END 语句块: 参见 awk 的工作原理
action 由一个或多个命令, 函数, 表达式组成, 之间由换行符或分号隔开, 并位于大括号内, 可以是如下几种, 或者什么都没有(print)
变量或数组赋值
输出命令
内置函数
控制流语句
awk 常见应用和工作原理
下面列出一个最常用的 awk 命令结构, 借此分析原理
awk 'BEGIN{ commands } pattern{ commands } END{ commands }'
首先执行 BEGIN {commands} 内的语句块, 注意这只会执行一次, 经常用于变量初始化, 头行打印一些表头信息, 只会执行一次, 在通过 stdin 读入数据前就被执行;
从文件内容中读取一行, 注意 awk 是以行为单位处理的, 每读取一行使用 pattern{commands} 循环处理 可以理解成一个 for 循环, 这也是最重要的部分;
最后执行 END{ commands } , 也是执行一次, 在所有行处理完后执行, 一帮用于打印一些统计结果.
第一个例子, 获得 / etc/passwd 文件种每行的地 1 个和第 7 个数据, 以逗号分隔, 并再第一行和最后一行打印一串文字.
- shell> cat /etc/passwd |awk -F ':' 'BEGIN {print"name,shell"} {print $1","$7} END {print"blue,/bin/nosh"}'
- name,shell
- root,/bin/bash
- daemon,/usr/sbin/nologin
- bin,/usr/sbin/nologin
- sys,/usr/sbin/nologin
- ...
- blue,/bin/nosh
书写注意事项
awk 后的命令需要用 单引号括起来
最好用 '{}' 括起来每个部分, 便于阅读;
每个 '{}' 可以有多个命令或者其它, 之间用 ';' 号分割.
怎么清晰的输出想要的信息?
awk 的输出主要靠 print,printf 指令, 这两个指令的用法和 c 语言中的 print,printf 一毛一样. awk 处理每行时是以列为每个域, 例如 print $1 就是输出第一列, print $1,$2 就是输出第 1,2 列, print $0 输出全部.
awk 怎么区分列呢, 默认是以空格区分, 但是你也可以通过 -F 参数指定, 例如 -F; 指定分号为分隔符,-F[;,] 指定分号和逗号为分隔符.
假设有如下一个文件 netstat.txt
- PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
- 2304 york 20 0 2244404 213596 84764 S 6.2 5.3 4:56.97 cinnamon
- 12489 york 20 0 43668 3708 2984 R 6.2 0.1 0:00.02 top
- 1 root 20 0 185352 5968 3972 S 0.0 0.1 0:04.35 systemd
- 2 root 20 0 0 0 0 S 0.0 0.0 0:00.05 kthreadd
- 4 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/0:0H
- 6 root 20 0 0 0 0 S 0.0 0.0 0:00.06 ksoftirqd/0
- 7 root 20 0 0 0 0 S 0.0 0.0 0:14.82 rcu_sched
- 34 root 20 0 0 0 0 S 0.0 0.0 0:00.04 khungtaskd
- 35 root 20 0 0 0 0 S 0.0 0.0 0:00.00 oom_reaper
- 36 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 writeback
- 37 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kcompactd0
1. 我只想打印第一列和第四列的内容
- shell> awk '{print $1,$2}' top.txt
- PID USER
- 2304 york
- 12489 york
- 1 root
- 2 root
- 4 root
- 6 root
- 7 root
- 34 root
- 35 root
- 36 root
- 37 root
2. 对齐不整齐, 打印的更漂亮些? 能加上行号?
来看看格式化输出 printf
- shell> awk '{printf"%-8s %-8s %-8s %-18s\n",NR, $1,$2,$12}' top.txt
- PID USER COMMAND
- 2304 york cinnamon
- 12489 york top
- 1 root systemd
- 2 root kthreadd
- 4 root kworker/0:0H
- 6 root ksoftirqd/0
- 7 root rcu_sched
- 34 root khungtaskd
- 35 root oom_reaper
- 36 root writeback
- 37 root kcompactd0
上面的对齐方式是不是更好看, 命令里面的 %-8s 用过 c 语言输出的一定很眼熟, 另外看到一个新的东西 NR 这是 awk 内部提供显示行号的变量, 除了这些还有以下常用的,(下面这张表就是用 awk 处理的)
变量 | 说明 |
---|---|
ARGC | 命令行参数的数目 |
ARGIND | 命令行中当前文件的位置(从 0 开始算) |
ARGV | 包含命令行参数的数组 |
CONVFMT | 数字转换格式(默认值为 %.6g) |
ENVIRON | 环境变量关联数组 |
ERRNO | 最后一个系统错误的描述 |
FIELDWIDTHS | 字段宽度列表(用空格键分隔) |
FILENAME | 当前输入文件的名 |
FNR | 同 NR,但相对于当前文件 |
FS | 字段分隔符(默认是任何空格) |
IGNORECASE | 如果为真,则进行忽略大小写的匹配 |
NF | 表示字段数,在执行过程中对应于当前的字段数 |
NR | 表示记录数,在执行过程中对应于当前的行号 |
OFMT | 数字的输出格式(默认值是 %.6g) |
OFS | 输出字段分隔符(默认值是一个空格) |
ORS | 输出记录分隔符(默认值是一个换行符) |
RS | 记录分隔符(默认是一个换行符) |
RSTART | 由 match 函数所匹配的字符串的第一个位置 |
RLENGTH | 由 match 函数所匹配的字符串的长度 |
SUBSEP | 数组下标分隔符(默认值是 34) |
3. 打印出的信息不够, 我要计算结果
例如上面的例子, 我想统计出所有进程总共占了多少 cpu,awk 变量和基本运算 了解一下, 先看例子
- shell> awk 'BEGIN {sum=0} {printf"%-8s %-8s %-18s\n", $1, $9, $11; sum+=$9} END {print"cpu sum:"sum}' top.txt
- PID %CPU TIME+
- 2304 6.2 4:56.97
- 12489 6.2 0:00.02
- 1 0.0 0:04.35
- 2 0.0 0:00.05
- 4 0.0 0:00.00
- 6 0.0 0:00.06
- 7 0.0 0:14.82
- 34 0.0 0:00.04
- 35 0.0 0:00.00
- 36 0.0 0:00.00
- 37 0.0 0:00.00
- cpu sum:12.4
又看到几个新的东西, 变量初始化及 awk 的一些基本运算
sum=0 一般都在 BEGIN 里面初始化一个变量, 如果不需要初始化可以直接进行对变量的赋值, 这很像脚本语言中的自动推断, 除了提供基本的运算以外(有哪些? c 有哪些这就基本有哪些), 还提供了一些高级计算函数, 例如数学运算
log,sqr,cos,sin
, 字符运算 length,substr
引入 awk 变量 除了能在内部使用, 还可以从外部引入, 使用 -v 参数指定, 例如下面是打印环境变量的例子, 这个在编写脚本中很常见.
awk -v var=$PATH 'BEGIN {print var}' top.txt
上面的例子引入了 awk 的运算, 知道可以定义变量运算, 除此之外还支持很多运算符, 算术运算符, 逻辑运算符, 甚至一些内部提供的函数.
信息太多, 我需要筛选
前面有说到 awk 是由 pattern 和 action 组成, 其中 pattern 部分就是能帮我们匹配或者过滤掉一些信息, 过滤方式有很多, 比如条件判断, 正则匹配, 甚至还可以和 c 语言一样写 if else, 到此就不要把 awk 当命令了, 它就是一门语言, 后面还有更高级的用法.
1. 去掉第一行, 只输出 cpu 消耗大于 0 的
- shell> awk 'NR>1 && $9>0 {printf"%-8s %-8s %-18s\n",$1,$9,$12}' top.txt
- 2304 6.2 cinnamon
- 12489 6.2 top
首先按照上面所介绍的 awk 执行流程来介绍, 单引号里面的 模式 + 命令 在每读到一行时就会执行, 判断条件也是如此 NR>1 && $9>0 这种写法和 c 语言没有两样, 只是少了判断 if 而已, 每读到一行时都执行这个判断条件来确定是否过滤; 下面转换成高级语言的代码.
- for l in lines {
- if ( NR>1 && $9>0 ) {
- printf ("%-8s %-8s %-18s\n",$1,$9,$12);
- }
- }
注意: 这个 NR>1 经常在数据库查询后被使用, 来剔除表头.
这个例子里面出现的就是 awk 的条件判断, 条件判断运算符也是和 c 语言一样不多阐述, 在比较时不仅可以比较数字还可以比较字符串, awk 会自动识别, 比较字符串时会按照 ASCII 码顺序比较.
2. 保留表头, 只输出特定用户名的进程
- awk 'NR==1 || /york/ {printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12}' top.txt
- PID USER %CPU COMMAND
- 2304 york 6.2 cinnamon
- 12489 york 6.2 top
上面 pattern 出现了一个新语法 /york/ , 这个就是正则匹配, 面对一些字符串匹配来进行过滤, 通过运算符显的很无力, 这在处理大量 log 时尤为突出, awk 也想到这点, 支持正则匹配来精准筛选; 正则过滤有好几种运用方法, 但主要格式都是 在双斜杠内写上你的正则表达式; 例如上面的例子就是 该行只要出现'york' 字符即可满足过滤条件
3. 上面例子不太准确, 更好的做法是针对域匹配
上面例子表述的是 改行只要出现 即可匹配到, 万一不是 USER 字段也有'york' 字符就会出现错误;
- shell> awk 'NR==1 || $2~/york/ {printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12}' top.txt
- PID USER %CPU COMMAND
- 2304 york 6.2 cinnamon
- 12489 york 6.2 top
上面引入了 ~ 运算符, 该运算符和 !~ 成对立关系, 类似 = 和!= 的关系, 前者表示匹配到的输出, 后者表示匹配到的过滤. 假设过滤'york' 的输出, 可以这样写:
- shell> awk 'NR==1 || $2!~/york/ {printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12}' top.txt
- PID USER %CPU COMMAND
- 1 root 0.0 systemd
- 2 root 0.0 kthreadd
- 4 root 0.0 kworker/0:0H
- 6 root 0.0 ksoftirqd/0
- 7 root 0.0 rcu_sched
- 34 root 0.0 khungtaskd
- 35 root 0.0 oom_reaper
- 36 root 0.0 writeback
- 37 root 0.0 kcompactd0
另外要注意的就是针对域匹配 $2~/york/, 前面有说过 $0 代表整个域, 所以 $0~/york/ 和 /york/ 是等价了, 说这个的原因就是当我们需要 针对一行做正则过滤的时候可以这样写 $0!~/york/, 这个在过滤日志的时候非常重要. 下面增加几个例子
- # 输出打印一行中出现 k 字符的行
- awk 'NR==1 || /k/ {printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12}' top.txt
- awk 'NR==1 || $0~/k/ {printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12}' top.txt
- # 过滤掉一行中出现 york 字符的行
- awk 'NR==1 || $0!~/york/ {printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12}' top.txt
- # 输出某个域字符以 k 开头的行
- awk 'NR==1 || $12~/^k/ {printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12}' top.txt
根据条件定制我的输出流程
信息过滤针对的是 pattern 做文章, 那么后面的 action 是不是也可以定制化控制, 答案肯定可以, action 可以做流程定制, 就是说我可以在里面使用 if else while 等流程控制语句. 这和上面的条件判断不一样, 因为他们针对的是不同部分, 前面用于信息过滤, 后面用于流程控制.
1. 过滤 cpu 大于 0 的, 我还可以这样写
- awk '{if($9>0){printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12}}' top.txt
- 2304 york 6.2 cinnamon
- 12489 york 6.2 top
条条大路通罗马不是吗? 这个例子就是流程控制的一个简单运用, 利用到的是 awk 的流程控制, 下面是流程控制的语法结构, 了解下:
- if (expression) {
- statement;
- statement;
- ... ...
- }
- if (expression) {
- statement;
- } else {
- statement2;
- }
- if (expression) {
- statement1;
- } else if (expression1) {
- statement2;
- } else {
- statement3;
- }
上面的例子在扩展下, 在结尾并统计下 cpu 的总和
- awk 'BEGIN {sum=0} {if($9>0){printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12; sum+=$9}} END {printf"cpu total usaged: %s\n", sum}' top.txt
- 2304 york 6.2 cinnamon
- 12489 york 6.2 top
- cpu total usaged: 12.4
2. 统计计算, 输出各个 USER 的进程数
- shell> awk 'NR!=1{a[$2]++;} END {for (i in a) print i", "a[i];}' top.txt
- york, 2
- root, 9
这个例子用一个数组统计不同用户的进程个数, 并在最后用循环打印出来, 这里有两个新的概念, 一个是另外一种流程控制循环, 另一个是数组的使用. 关于循环的控制语法如下, 和其它高级语言都类似.
while(表达式)
{语句}
for(变量 in 数组)
{语句}
for(变量; 条件; 表达式)
{语句}
do
{语句} while(条件)
除此之外, 流程控制还有 break, continue, exit 等关键字, 这些关键字含义和 c 语言一毛一样.
上面例子中 a[$2] 是典型的一种数组使用方法, 用编程语言来看, 这个叫数组似乎不大妥当, 理解成 map 更合适, 更像是 key-value 的存储结构.
3. 怎样使用数组
上面看到了数组的基本使用, 其实 awk 给数组赋予了很多功能, 和很多高级脚本语言一样, 提供了相关的函数获取长度, 排序等, 另外存储是 key-value 结构, 能像 map 一样判断 key 是否存在.
获取长度 length
- shell> awk 'BEGIN{info="it is a test";lens=split(info,tA," ");print length(tA),lens;}'
- 4 4
上面 split 函数用于字符串分割, 和 c 语言的又是一毛一样
循环输出
- shell> awk 'BEGIN{info="it is a test";split(info,tA," ");for(k in tA){print k,tA[k];}}'
- 4 test
- 1 it
- 2 is
- 3 a
由于是 key-value 的存储结构, 所以使用这样的 for 循环输出结果是无序的,
有序输出
- shell> awk 'BEGIN{info="it is a test";tlen=split(info,tA," ");for(k=1;k<=tlen;k++){print k,tA[k];}}'
- 1 it
- 2 is
- 3 a
- 4 test
注意: 数组下标从 1 开始的
注意: 这种输出方法仅适用于把数组真正当作 '数组' 使用, key 值就是自然递增的数, 而不是当 map
判断是否存在 key in array
- shell> awk 'BEGIN{tB["a"]="a1";tB["b"]="b1";if("c"in tB){print"ok";};for(k in tB){print k,tB[k];}}'
- a a1
- b b1
注意: 判断语法是 key in array 不能直接写 array[key] != value 形式, 如果这样写它会默认创建一个, 使用过高级脚本语言的都知道;
删除 deletekey
- shell> awk 'BEGIN{tB["a"]="a1";tB["b"]="b1";delete tB["a"];for(k in tB){print k,tB[k];}}'
- b b1
搞掂, 保存下来
这里引入怎么将处理后的文本保存下来, awk 提供了重定向的的功能;
1. 将上面例子中 cpu 大于 0 的保存到 cpu.txt 文件
- shell> awk 'BEGIN {sum=0} {if($9>0){printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12>"cpu.txt"; sum+=$9}} END {printf"cpu total usaged: %s\n", sum>"cpu.txt"}' top.txt
- shell> cat cpu.txt
- 2304 york 6.2 cinnamon
- 12489 york 6.2 top
- cpu total usaged: 12.4
上面引入的> 就是重定向符, 使用方法很简单, 在你想要输出到文件的 print 命令后加上> "filename" 即可.
2. 提点小要求, 拆分文件存储, 按 USER 拆分
- shell> awk 'NR>1 {printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12> $2}' top.txt
- shell> cat york
- 2304 york 6.2 cinnamon
- 12489 york 6.2 top
- shell> cat root
- 1 root 0.0 systemd
- 2 root 0.0 kthreadd
- 4 root 0.0 kworker/0:0H
- 6 root 0.0 ksoftirqd/0
- 7 root 0.0 rcu_sched
- 34 root 0.0 khungtaskd
- 35 root 0.0 oom_reaper
- 36 root 0.0 writeback
- 37 root 0.0 kcompactd0
上面按照'USER' 简单做了文件拆分, 将输出内容拆分到'york'和'root' 两个文件中, 这个技巧在后面数据归类或者日志归类中使用非常频繁.
3. 要求在高点, 根据字符匹配来确定文件拆分 (结合 if-else 语句)
shell> awk 'NR>1 {if($0~/york/){printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12>"1.txt"}else if($0~/root/){printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12>"2.txt"}else{printf"%-8s %-8s %-8s %-18s\n",$1,$2,$9,$12>"3.txt"}}' top.txt
上面将生成 3 个文件, 其中 USER=york 的在'1.txt',USER=root 在'2.txt', 其它在'3.txt', 上面的例子就是前面所介绍的一种结合, 正则匹配其实也可以用在 action 中做流程判断.
附录: 实例记录
最后了解原理和过程后, 发现一切都是水到渠成, 其实不然, 配合其它命令 (比如管道, 排序等) 可以实现很多不可思议的工作, 这里专门积累一些平时用到的.
- # 按连接数查看客户端 IP
- netstat -ntu | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr
- (欢迎各位对文章中的错误指正, 另外如果小伙伴们有好的实例来分享, 谢谢各位)
来源: https://www.qcloud.com/developer/article/1159061