awk 可以把一行文本拆成多个字段,再按字段筛选、计算或输出。平时查看日志,想取出 IP、访问时间这些内容,用 awk 就很方便。
默认情况下,awk 按换行读取记录,按连续的空格或 Tab 拆分字段。可以直接读取文件,也可以通过管道处理前一个命令的输出。基本形式是:
awk '条件 {动作}' 文件名省略条件,动作就对每条记录执行;省略动作,则输出符合条件的整条记录。
从日志中取字段
以 Nginx 访问日志为例,把下面五行示例保存为 access.log:
192.0.2.10 - - [21/Jul/2017:14:22:48 +0800] "GET / HTTP/1.1" 200 2311 "-" "curl/7.29.0"
192.0.2.10 - - [21/Jul/2017:14:22:49 +0800] "GET /css/global.css HTTP/1.1" 200 1033 "-" "curl/7.29.0"
192.0.2.20 - - [21/Jul/2017:14:25:45 +0800] "GET /missing HTTP/1.1" 404 162 "-" "curl/7.29.0"
192.0.2.30 - - [21/Jul/2017:14:26:29 +0800] "GET / HTTP/1.1" 200 2311 "-" "curl/7.29.0"
192.0.2.40 - - [21/Jul/2017:14:26:30 +0800] "GET /favicon.ico HTTP/1.1" 404 162 "-" "curl/7.29.0"取最后五行,输出 IP 和访问时间:
tail -n 5 access.log | awk '{print $1 "\t" $4}'结果为:
192.0.2.10 [21/Jul/2017:14:22:48
192.0.2.10 [21/Jul/2017:14:22:49
192.0.2.20 [21/Jul/2017:14:25:45
192.0.2.30 [21/Jul/2017:14:26:29
192.0.2.40 [21/Jul/2017:14:26:30$1、$4 分别表示第一个和第四个字段,$0 表示整条记录。这里 $4 带着左方括号,时区则在 $5 中。
只看现有的最后五行,用 tail -n 5 即可;需要持续查看新增日志时,再用 tail -n 5 -f。后面的示例都处理固定内容,方便对照结果。
加上筛选条件
只输出 IP 为 192.0.2.10 的记录,在动作前加上条件:
tail -n 5 access.log | awk '$1 == "192.0.2.10" {print $1 "\t" $4}'结果为:
192.0.2.10 [21/Jul/2017:14:22:48
192.0.2.10 [21/Jul/2017:14:22:49如果想看这些请求的完整日志,就省略后面的动作:
awk '$1 == "192.0.2.10"' access.log这里输出的就是示例文件的前两行。
NR、NF 和 FS
awk 有几个常用的内置变量:
| 变量 | 含义 |
|---|---|
| NR | 已读取的记录总数;默认按行读取时,就是当前读到的行数 |
| NF | 当前记录的字段数 |
| FS | 输入字段分隔符 |
把行号和字段数一起打印出来:
tail -n 5 access.log | awk '{print $1 "\trow:" NR "\tcolumns:" NF}'结果为:
192.0.2.10 row:1 columns:12
192.0.2.10 row:2 columns:12
192.0.2.20 row:3 columns:12
192.0.2.30 row:4 columns:12
192.0.2.40 row:5 columns:12这里的 NR 从管道传入的第一行开始计数,不是这条日志在原文件中的行号。
awk 默认不会把引号中的内容当成一个整体。比如日志里的 "GET / HTTP/1.1" 仍然占三个字段;User-Agent 中如果有空格,也会继续拆分,所以真实日志的 NF 不一定相同。
在 BEGIN 中设置分隔符
/etc/passwd 使用冒号分隔字段,第一个字段是账号名,第三个字段是 UID。为了让结果固定,下面用一个示例文件 users.txt:
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
app:x:1000:1000:app:/home/app:/bin/bash
worker:x:1001:1001:worker:/home/worker:/bin/bash设置 FS 时,下面这种写法有个问题:
awk '{FS=":"} {print $1 "\t" $3}' users.txt普通动作在读入记录之后才执行。第一行读入时还是默认分隔符,此时再修改 FS,不会按新分隔符重新拆分这一行。
把初始化放到 BEGIN 中,就能在读取第一行之前设置好:
awk 'BEGIN {FS=":"} {print $1 "\t" $3}' users.txt结果为:
root 0
bin 1
daemon 2
app 1000
worker 1001指定分隔符也可以直接用 -F,写起来更短:
awk -F ':' '{print $1 "\t" $3}' users.txt在 END 中汇总
BEGIN 在读取数据之前执行,END 在输入结束后执行。统计 UID 大于 20 的记录数:
awk 'BEGIN {FS=":"; count=0} $3>20 {count++} END {printf "total is : %d\n", count}' users.txt结果为:
total is : 2BEGIN 中的两条赋值语句用分号分隔。每读到一条符合条件的记录,count 加一,最后通过 printf 输出整数和换行。
直接统计本机账号时,把 users.txt 换成 /etc/passwd 即可,数量以本机数据为准。END 要等输入结束才执行,所以这种一次性统计不要接在持续等待新日志的 tail -f 后面。