深色模式
Shell 文本处理 awk 实战
摘要:awk 是处理「一行一条记录」文本最快的工具:按列取字段、按条件过滤、按某个字段分组求和。本文用访问日志和进程列表两个真实场景演示,从单行命令写到可复用的 awk 脚本。
适用环境
bash
awk --version | head -1 || awk -W version
# 准备一份练习数据(Nginx 访问日志格式)
head -3 /var/log/nginx/access.log 2>/dev/null || echo "将自行构造样例"1
2
3
2
3
操作步骤
一、核心模型:awk 是「逐行 + 模式-动作」
bash
awk '模式 { 动作 }' file
# 模式为真才执行动作;省略模式则每行都执行1
2
2
内置变量:$0 整行、$1..$n 第 n 列、NF 列数、NR 行号、FS 输入分隔符、OFS 输出分隔符。
二、取列与格式化
bash
# 取第 1 列和第 7 列(IP 与状态码)
awk '{print $1, $7}' /var/log/nginx/access.log
# 自定义分隔符(如 /etc/passwd 的冒号)
awk -F: '{print $1, $3, $7}' /etc/passwd
# 取最后一列
awk '{print $NF}' file
awk '{print $(NF-1)}' file
# 美化输出:printf + 表头
awk 'BEGIN{printf "%-16s %-8s %s\n","IP","STATUS","URL"}
{printf "%-16s %-8s %s\n",$1,$9,$7}' /var/log/nginx/access.log | head -201
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
三、条件过滤
bash
# 状态码为 5xx 的请求
awk '$9 >= 500 && $9 < 600 {print $1, $7, $9}' /var/log/nginx/access.log
# 包含关键字的行
awk '/ERROR|FATAL/ {print NR": "$0}' /var/log/app/app.log
# 第 10 列(响应字节)大于 1MB 的请求
awk '$10 > 1048576 {print $7, $10}' /var/log/nginx/access.log
# 只处理前 100 行 / 跳过第一行表头
awk 'NR <= 100 {print}' file
awk 'NR > 1 {print}' file1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
四、分组统计(awk 最擅长的场景)
bash
# 统计各状态码数量
awk '{c[$9]++} END{for (k in c) printf "%s\t%d\n", k, c[k]}' \
/var/log/nginx/access.log | sort -rn -k2
# 统计每个 IP 的请求数,取 Top 10
awk '{c[$1]++} END{for (ip in c) print c[ip], ip}' /var/log/nginx/access.log \
| sort -rn | head -10
# 统计每个 URL 的总流量(字节求和)
awk '{b[$7]+=$10} END{for (u in b) printf "%.2f MB\t%s\n", b[u]/1048576, u}' \
/var/log/nginx/access.log | sort -rn | head -10
# 求平均值与最大值(响应时间示例)
awk '{sum+=$NF; if ($NF>max) max=$NF; n++}
END{printf "avg=%.3f max=%.3f count=%d\n", sum/n, max, n}' rt.txt1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
五、多分隔符与字段提取
bash
# 同时以空格和方括号为分隔符,提取日志时间
awk -F'[][ ]+' '{print $4}' /var/log/nginx/access.log | head -3
# 从 ps 输出里提取进程的 RSS 之和(MB)
ps -eo rss,comm --no-headers | awk '{s+=$1} END{printf "%.1f MB\n", s/1024}'1
2
3
4
5
2
3
4
5
六、BEGIN / END 与传参
bash
# 通过 -v 传入变量,让脚本更通用
awk -v threshold=500 '$9 >= threshold {print}' /var/log/nginx/access.log
# BEGIN 里设置分隔符与输出格式
awk 'BEGIN{FS=","; OFS="|"; print "name|size"} {print $1,$2}' data.csv
# END 里输出汇总
awk '{s+=$2} END{print "总计:", s}' data.csv1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
七、写成可复用的 awk 脚本
bash
cat > /usr/local/bin/top-url.awk <<'EOF'
#!/usr/bin/awk -f
# 用法: awk -f top-url.awk access.log | sort -rn | head
{
count[$7]++
bytes[$7] += $10
}
END {
for (u in count) {
printf "%d\t%.2f\t%s\n", count[u], bytes[u]/1048576, u
}
}
EOF
chmod +x /usr/local/bin/top-url.awk
awk -f /usr/local/bin/top-url.awk /var/log/nginx/access.log | sort -rn | head -101
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
八、实战:一分钟出一份访问报表
bash
#!/usr/bin/env bash
# daily-report.sh <access.log>
set -euo pipefail
LOG="${1:?需要传入日志文件}"
echo "=== 总请求数 ==="
awk 'END{print NR}' "$LOG"
echo "=== 状态码分布 ==="
awk '{c[$9]++} END{for(k in c) printf "%s\t%d\n", k, c[k]}' "$LOG" | sort -k2 -rn
echo "=== 平均响应字节 ==="
awk '{s+=$10} END{printf "%.0f\n", s/NR}' "$LOG"
echo "=== Top 10 IP ==="
awk '{c[$1]++} END{for(i in c) print c[i], i}' "$LOG" | sort -rn | head -101
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
注意
awk 的数组遍历顺序是不确定的,输出结果必须再 sort 一次才稳定。这是新手常犯的「每次跑顺序都不一样」的困惑来源。
验证
- [ ]
awk '{print $1}'能正确取到第一列 - [ ] 状态码统计结果与
grep -c抽样核对一致 - [ ] 字节求和与
du/wc等其它方式交叉验证量级一致 - [ ] awk 脚本文件可执行,且
sort后输出稳定
常见坑
- 默认分隔符是任意空白:连续空格会被当成一个分隔符,但
awk '{print $3}'在字段数不固定时容易取错列,应先用NF判断。 - 字符串与数字比较:awk 会自动类型转换,
$9 == "500"和$9 == 500都成立,但排序时可能按字符串排,需显式+0。 - 未初始化的数组元素:
c[x]++从空值自增是安全的,但if (c[x] > 0)在 x 不存在时会创建该键。 - NR 与 FNR 混用:同时处理多个文件时
NR是累计行号,FNR才是当前文件行号,判断表头要用FNR==1。 - 日志含空格的字段:如 User-Agent 带空格,
$NF取到的不是完整 UA,需要按引号或自定义分隔符处理。