【前情提要】
grep 练熟了,小林能从 10 万行日志里快速筛出想要的行。老周说:“grep 是筛子,但你要统计,还得靠 awk。”小林打开 awk 文档,准备学字段处理。但他不知道,$1、$NF、-F、NR、END,每一个都有用。
一、老周的第二把刀
早上到工位,老周问小林:
“昨天你用 grep 统计了 404 数量,怎么统计的?”
“grep -c " 404 "。”
“那如果我想统计每个 IP 的请求数呢?”
小林想了想:“grep 做不到吧?它只能筛行,不能按 IP 分组。”
“对。”老周说,“grep 是筛子,负责‘找出你想要的行’。但你要‘把行拆开,按字段统计’,就得用第二把刀——awk。”
“awk 是啥?”
“你可以把它理解成‘按列处理文本的语言’。”老周说,“grep 按行匹配,awk 按列处理。日志是一行一行的,每一行有多个字段,awk 就是用来拆这些字段的。”
小林打开 awk 手册,准备逐条查。
老周拦住他:“别查。跟 grep 一样,直接上手。你先告诉我,access.log 的一行,你能看到几列?”
小林敲:
sudo tail -1 /var/log/nginx/access.log
输出:
192.168.1.100 - - [14/Sep/2026:10:00:01 +0800] "GET / HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
“空格分隔,大概十几列。”小林说。
“那你想取第一列,怎么取?”
“awk '{print $1}'?”
“对。”老周说,“这就是 awk 最基本的用法。”
二、awk 的基本逻辑:按列处理
“awk 的逻辑,比 grep 复杂一点,但核心就一句话。”老周说。
awk '模式 {动作}' 文件
“模式”决定哪些行要处理,“动作”决定对这些行做什么。“如果模式省略,就是所有行。”
“你刚才写的 awk '{print $1}',就是‘对所有行,打印第一列’。”
小林试:
sudo awk '{print $1}' /var/log/nginx/access.log | head -5
输出:
192.168.1.100
192.168.1.101
192.168.1.100
192.168.1.102
192.168.1.100
“这就是所有 IP。”小林说。
“对。但 awk 默认按空格和 Tab 分隔。你的日志是空格分隔,所以 $1 是 IP,$2 是 -,$3 是 -,$4 是时间……”
“那我怎么知道 $9 是状态码?”
“你打印出来看。”
sudo awk '{for(i=1;i<=NF;i++) print i": "$i}' /var/log/nginx/access.log | head -20
输出:
1: 192.168.1.100
2: -
3: -
4: [14/Sep/2026:10:00:01
5: +0800]
6: "GET
7: /
8: HTTP/1.1"
9: 200
10: 1234
11: "-"
12: "Mozilla/5.0"
“看到了吗?”老周说,“NF 是字段总数,for 循环从 1 到 NF,打印每个字段。你以后不确定字段位置,先用这个命令打印出来。”
小林记下:
awk '{print $N}':打印第 N 列NF:字段总数$NF:最后一列$(NF-1):倒数第二列for(i=1;i<=NF;i++):遍历所有字段
三、awk 常用参数和变量
“现在过一遍常用的。”老周说。
-F:指定分隔符
“日志默认空格分隔,但有些日志是逗号、冒号、竖线分隔。用 -F 指定。”
echo "a:b:c" | awk -F: '{print $2}'
输出:
b
“-F: 表示用冒号分隔。”老周说,“你以后处理 /etc/passwd,就是 awk -F: '{print $1}'。”
NR:行号
“你想在输出里带行号。”
sudo awk '{print NR": "$1}' /var/log/nginx/access.log | head -5
输出:
1: 192.168.1.100
2: 192.168.1.101
3: 192.168.1.100
4: 192.168.1.102
5: 192.168.1.100
“NR 是当前行号。”老周说,“NR==1 可以匹配第一行,NR<=10 匹配前 10 行。”
END:处理完所有行之后执行
“你想统计总数。”
sudo awk 'END {print NR}' /var/log/nginx/access.log
输出:
102347
“END 块在所有行处理完后执行,NR 是总行数。”老周说,“END 是 awk 做统计的关键。”
BEGIN:处理之前执行
“你想在处理前做点事,比如打印表头。”
sudo awk 'BEGIN {print "IP"} {print $1}' /var/log/nginx/access.log | head -5
输出:
IP
192.168.1.100
192.168.1.101
...
“BEGIN 在处理前执行一次,END 在处理后执行一次。”老周说,“中间的主块,对每一行执行一次。”
小林记下:
-F:指定分隔符NR:行号NF:字段数$NF:最后一列BEGIN:处理前执行END:处理后执行
四、实战一:统计 Top IP
“现在实战。”老周说,“统计访问量最高的 IP。”
小林想了想:“awk '{print $1}' | sort | uniq -c | sort -rn | head。”
“对。”老周说,“你敲一下。”
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
输出:
5000 192.168.1.100
1200 192.168.1.101
800 192.168.1.102
300 192.168.1.103
100 192.168.1.104
“192.168.1.100 访问 5000 次,最多。”小林说。
“那如果我不用管道,直接用 awk 统计呢?”
“可以。”老周说,“awk 支持数组。”
sudo awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' /var/log/nginx/access.log | sort -rn | head -10
输出一样。
“count[$1]++ 表示用第一列做 key,计数加一。END 里遍历数组,打印计数和 IP。”老周说,“这是 awk 原生的统计方式,不用 sort | uniq -c。”
“那哪个更好?”
“都行。”老周说,“管道写法更直观,awk 数组写法更灵活。你入门阶段,先用管道写法,等熟练了再学数组。”
小林记下:
# 管道写法
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
# awk 数组写法
awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' access.log | sort -rn | head
五、实战二:统计 Top URL
“统计访问量最高的 URL。”
sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
输出:
3000 /posts/my-first-post/
1500 /
800 /tags/hugo/
500 /about/
300 /images/hugo-logo.png
“你的文章访问量最高,首页第二。”老周说,“这是正常分布。”
“那如果我只要 200 的 URL 呢?”
“先筛状态码。”
sudo awk '$9 == 200 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
“awk 的模式部分,可以写条件。 $9 == 200 表示只处理状态码为 200 的行。”
六、实战三:统计状态码分布
“统计所有状态码的分布。”
sudo awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
输出:
95000 200
5000 404
1500 301
500 500
347 502
“200 占绝大多数,404 有 5000,500 有 500。”小林说。
“那如果我只看 4xx 和 5xx 呢?”
“用 awk 条件。”
sudo awk '$9 >= 400 {print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
输出:
5000 404
500 500
347 502
“$9 >= 400 表示状态码大于等于 400。”老周说,“awk 支持比较、逻辑运算。 $9 >= 400 && $9 < 500 就是所有 4xx。”
小林记下:
# 状态码分布
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# 只看 4xx 和 5xx
awk '$9 >= 400 {print $9}' access.log | sort | uniq -c | sort -rn
# 只看 4xx
awk '$9 >= 400 && $9 < 500 {print $9}' access.log | sort | uniq -c | sort -rn
七、实战四:算平均响应大小
“$10 是响应大小。”老周说,“你算一下平均响应大小。”
“awk '{sum += $10} END {print sum/NR}'?”
“对。”
sudo awk '{sum += $10} END {print sum/NR}' /var/log/nginx/access.log
输出:
1234.56
“平均响应 1234 字节。”小林说。
“但你注意,$10 可能是 -,表示没有响应体。- 不是数字,awk 会当成 0。”老周说,“所以你要加条件。”
sudo awk '$10 != "-" {sum += $10; count++} END {print sum/count}' /var/log/nginx/access.log
“这样只算有响应体的请求。”老周说,“awk 做数值计算,先确认字段是数字。”
八、awk 的常用内置变量
老周拿过小林的笔记本,写了一个表。
| 变量 | 含义 |
|---|---|
$0 |
整行 |
$1、$2…… |
第 1、2……列 |
$NF |
最后一列 |
NF |
字段总数 |
NR |
当前行号 |
FNR |
当前文件的行号(多文件时区分) |
FS |
输入字段分隔符,默认空格 |
OFS |
输出字段分隔符,默认空格 |
RS |
输入行分隔符,默认换行 |
ORS |
输出行分隔符,默认换行 |
“常用的就这几个。”老周说,“$0、$1、$NF、NF、NR、FS。你先把这六个记死。”
“那 FS 和 -F 有啥区别?”
“-F 是命令行参数,FS 是 awk 内置变量。效果一样,但 FS 可以在 BEGIN 里改。”
awk 'BEGIN {FS=":"} {print $1}' /etc/passwd
“等价于 awk -F: '{print $1}'。”老周说,“你以后在脚本里,用 FS 更灵活。”
小林记下六个核心变量:$0、$1、$NF、NF、NR、FS。
九、排错挑战:awk 统计结果全是 1
下午,小林想统计每个 URL 的状态码分布。他写:
sudo awk '{print $7, $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
输出:
1 / 200
1 / 404
1 /posts/my-first-post/ 200
1 /posts/my-first-post/ 404
1 /api/health 500
...
“全是 1?”小林懵了,“每个组合只出现一次?”
他检查日志:
sudo tail -5 /var/log/nginx/access.log
输出:
192.168.1.100 - - [14/Sep/2026:10:00:01 +0800] "GET / HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
192.168.1.100 - - [14/Sep/2026:10:00:01 +0800] "GET / HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
192.168.1.100 - - [14/Sep/2026:10:00:01 +0800] "GET / HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
“日志里明明有重复的 / 200,为什么统计出来是 1?”
他喊来老周。
老周看了一眼命令:“你 print $7, $9,中间是逗号。awk 输出时,默认用空格分隔。但你的日志里,$7 和 $9 之间可能已经有空格了,所以输出里 $7 和 $9 之间是空格,但 uniq -c 是按整行去重。问题是——你 $7 和 $9 之间,是不是有别的字段?”
小林仔细看:
/posts/my-first-post/ 200
“看起来是 /posts/my-first-post/ 和 200,中间一个空格。”
“那你 print $7, $9 输出是什么?”
小林单独试:
sudo awk '{print $7, $9}' /var/log/nginx/access.log | head -5
输出:
/ 200
/ 200
/posts/my-first-post/ 200
/posts/my-first-post/ 200
/api/health 500
“看起来没问题啊。”
老周凑近看:“你 uniq -c 之前,有没有 sort?”
“有啊,sort | uniq -c。”
“那你 sort 的时候,/ 200 和 /posts/my-first-post/ 200 排序,/ 在 /posts 前面,没问题。但你看输出里,/ 200 和 /posts/my-first-post/ 200 是分开的,说明 sort 没把它们归到一起。”
“为啥?”
“因为空格数量可能不一样。”老周说,“awk 输出时,print $7, $9 用 OFS 分隔,默认是一个空格。但你的 $7 里可能包含空格?不对,$7 是 URL,没有空格。那问题在哪?”
老周仔细看:
1 / 200
“等等,这个 1 是什么?”
小林说:“uniq -c 输出的计数啊。”
“不对。”老周说,“uniq -c 的输出格式是‘计数 内容’。你这里 1 / 200,说明 uniq -c 认为这一行只出现了一次。但你日志里明明有多次 / 200。”
老周拿过键盘,敲:
sudo awk '{print $7, $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -5
输出:
1 / 200
1 / 404
1 /posts/my-first-post/ 200
“你看,sort 之后,/ 200 和 / 200 应该挨在一起,uniq -c 应该统计成 3 / 200。但它统计成 1 / 200,说明 sort 没把它们排在一起。”
“为什么?”
“因为你 sort 的时候,/ 200 这一行的实际内容,可能不是你想的那样。”老周说,“你 awk '{print $7, $9}' 输出的是 / 200,但 sort 是按整行排序。如果两行看起来一样,但实际有不可见字符,sort 就会认为它们不同。”
“那怎么检查?”
“用 cat -A。”
sudo awk '{print $7, $9}' /var/log/nginx/access.log | head -3 | cat -A
输出:
/ 200$
/ 200$
/posts/my-first-post/ 200$
“$ 是行尾标记,看起来没有不可见字符。”小林说。
“那你再看这个。”
sudo awk '{print $7, $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
老周指着输出:“你看,/ 200 的计数是 1,但 /posts/my-first-post/ 200 的计数也是 1。你的日志里,/ 和 /posts/ 应该都有很多次。为什么全是 1?”
小林突然想到:“我日志是不是被 logrotate 切了?access.log 现在是新的,只有几条?”
“你 wc -l 看看。”
sudo wc -l /var/log/nginx/access.log
输出:
5 /var/log/nginx/access.log
“5 行。”小林说,“原来日志被切了,新的 access.log 只有 5 行,所以每个组合只出现一次。”
“对。”老周说,“你分析日志之前,先 wc -l 确认数据量。 如果日志被切了,你要分析的是 access.log.1 或者 .gz 文件。”
小林打开旧日志:
sudo zcat /var/log/nginx/access.log.2.gz | awk '{print $7, $9}' | sort | uniq -c | sort -rn | head -10
输出:
3000 /posts/my-first-post/ 200
1500 / 200
800 /tags/hugo/ 200
500 /about/ 200
300 /images/hugo-logo.png 200
“这才是正确的统计。”小林说。
排错思路总结:
awk统计结果全是 1,先wc -l确认日志行数。- 如果行数很少,说明日志被
logrotate切了。 - 分析旧日志要用
access.log.1或zcat access.log.2.gz。 sort之前确认数据量,别对空文件或极少行做统计。cat -A检查不可见字符,但优先检查数据量。- 分析日志前,先
wc -l,再head,确认数据对不对。
“记住,”老周说,“日志分析的第一原则:先确认数据。 你 wc -l 看行数,head 看格式,tail 看时间。这三步做完,再开始统计。不然你统计出来的全是 1,还以为是命令写错了。”
十、练习
- 练习一:用
awk '{print $1}' | sort | uniq -c | sort -rn | head -10统计access.log中访问量最高的前 10 个 IP。用awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' | sort -rn | head -10做同样的统计,对比结果。 - 练习二:用
awk '$9 == 404 {print $7}' | sort | uniq -c | sort -rn | head -10统计 404 最多的 URL。用awk '$9 >= 400 {print $9}' | sort | uniq -c | sort -rn统计所有 4xx 和 5xx 状态码分布。 - 练习三:用
awk '{sum += $10} END {print sum/NR}'计算平均响应大小。再用awk '$10 != "-" {sum += $10; count++} END {print sum/count}'计算排除-后的平均响应大小。对比两个结果。
参考答案
练习一:
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
sudo awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' /var/log/nginx/access.log | sort -rn | head -10
练习二:
sudo awk '$9 == 404 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
sudo awk '$9 >= 400 {print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
练习三:
sudo awk '{sum += $10} END {print sum/NR}' /var/log/nginx/access.log
sudo awk '$10 != "-" {sum += $10; count++} END {print sum/count}' /var/log/nginx/access.log
十一、下集预告
awk 练熟了,小林能从日志里按字段统计 Top IP、Top URL、状态码分布。老周说:“grep 筛,awk 算,但如果你想‘改’日志内容呢?比如脱敏 IP、提取时间段、删除空行。”小林打开 sed 文档,准备学第三把刀。但他不知道,s///、-i、-n、p、d,每一个都有讲究。
下一集:《sed 实战:批量“修改”日志和配置》