第 5 章:AWK 进阶
第 5 章:AWK 进阶
如果说 AWK 基础让你能处理行,那么 AWK 进阶让你能处理整个数据世界。
5.1 关联数组
AWK 的数组是关联数组(类似 Python 的字典),下标可以是任意字符串。
基本操作
# 赋值
awk 'BEGIN {
fruits["apple"] = 5
fruits["banana"] = 3
fruits["cherry"] = 8
for (f in fruits) print f, fruits[f]
}'
# 检查元素是否存在
awk 'BEGIN {
fruits["apple"] = 5
if ("apple" in fruits) print "apple exists"
if ("grape" in fruits) print "grape exists"
}'
# 删除元素
awk 'BEGIN {
fruits["apple"] = 5
delete fruits["apple"]
print length(fruits)
}'
多维数组(模拟)
AWK 没有真正的多维数组,但用 SUBSEP 模拟:
# 用逗号分隔的下标(SUBSEP 模拟)
awk 'BEGIN {
matrix[1,1] = "A"
matrix[1,2] = "B"
matrix[2,1] = "C"
matrix[2,2] = "D"
for (idx in matrix) {
split(idx, sep, SUBSEP)
print "matrix["sep[1]","sep[2]"] = " matrix[idx]
}
}'
🏢 业务场景:频率统计
cat > access.log << 'EOF'
192.168.1.1 GET /index.html 200
192.168.1.2 GET /about.html 200
192.168.1.1 POST /login 302
192.168.1.3 GET /index.html 200
192.168.1.1 GET /dashboard 200
192.168.1.2 GET /missing.html 404
EOF
# 统计各 IP 的请求数
$ awk '{count[$1]++} END{for(ip in count) printf "%-15s %d\n", ip, count[ip]}' access.log
# 统计各状态码数量
$ awk '{count[$3]++} END{for(s in count) print s, count[s]}' access.log
# 找出请求最多的 IP
$ awk '{count[$1]++} END{max=0; for(ip in count) {if(count[ip]>max) {max=count[ip]; top=ip}} print top, max}' access.log
数组遍历顺序
# AWK 不保证遍历顺序!需要排序请用管道
awk '{count[$1]++} END{for(ip in count) print count[ip], ip}' file | sort -rn
# GNU AWK 的 PROCINFO["sorted_in"] 可以控制遍历顺序
awk 'BEGIN{PROCINFO["sorted_in"]="@val_num_desc"}
{count[$1]++} END{for(ip in count) print count[ip], ip}' file
🏢 业务场景:分组统计
cat > sales.csv << 'EOF'
Alice,Electronics,1500
Bob,Clothing,800
Alice,Electronics,2000
Carol,Food,500
Bob,Electronics,1200
Alice,Food,300
EOF
# 按人统计总销售额
$ awk -F, '{total[$1]+=$3} END{for(p in total) printf "%-10s %10.2f\n", p, total[p]}' sales.csv
# 按类别统计总销售额
$ awk -F, '{total[$2]+=$3} END{for(c in total) printf "%-15s %10.2f\n", c, total[c]}' sales.csv
# 按人和类别统计(二维键)
$ awk -F, '{
key=$1","$2
total[key]+=$3
count[key]++
} END {
for(k in total) {
split(k, a, ",")
printf "%-10s %-15s 总额: %10.2f 次数: %d\n", a[1], a[2], total[k], count[k]
}
}' sales.csv
5.2 用户自定义函数
函数定义
# 基本语法
awk '
function my_func(param1, param2) {
# 函数体
return result
}
{
result = my_func($1, $2)
print result
}' file
函数示例
# 绝对值函数
awk '
function abs(x) { return (x < 0) ? -x : x }
{ print abs($1) }
' data.txt
# 最大值函数
awk '
function max(a, b) { return (a > b) ? a : b }
{ current_max = max(current_max, $1) }
END { print "最大值:", current_max }
' data.txt
# 格式化字节大小
awk '
function human_size(bytes) {
if (bytes >= 1073741824) return sprintf("%.2f GB", bytes/1073741824)
if (bytes >= 1048576) return sprintf("%.2f MB", bytes/1048576)
if (bytes >= 1024) return sprintf("%.2f KB", bytes/1024)
return sprintf("%d B", bytes)
}
{ print $1, human_size($2) }
' data.txt
# 去除首尾空白
awk '
function trim(s) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", s)
return s
}
{ print trim($0) }
' file.txt
🏢 业务场景:日志时间解析
# 解析日志时间差
awk '
function parse_ts(ts) {
# 假设格式: 15/Jan/2024:10:23:45
gsub(/[\[\]\/:]/, " ", ts)
split(ts, d, " ")
# 简化处理,返回秒数
return d[4]*3600 + d[5]*60 + d[6]
}
{
ts = parse_ts($4)
if (prev_ts > 0 && ts - prev_ts > 60) {
print "间隔超过 1 分钟: " prev_line " -> " $0
}
prev_ts = ts
prev_line = $0
}' access.log
函数的作用域
# AWK 的变量默认是全局的
# 使用函数参数来避免命名冲突
# ❌ 不好 — 全局变量可能冲突
awk '{
temp = $1 + 1
result = temp * 2
print result
}'
# ✅ 好 — 使用函数参数
awk '
function calculate(x, temp) {
temp = x + 1 # temp 是局部变量(因为它是参数)
return temp * 2
}
{ print calculate($1) }
' file
# 💡 技巧:额外的参数名用作局部变量声明
awk '
function process(data, i, n, arr, result) {
# i, n, arr, result 都是局部变量
n = split(data, arr, ",")
for (i = 1; i <= n; i++) result += arr[i]
return result
}
' file
📌 重要:AWK 没有
local关键字。函数参数列表中多出来的参数名会被当作局部变量使用。这是 AWK 的独特约定。
5.3 内置函数
字符串函数
| 函数 | 说明 | 示例 |
|---|---|---|
length(s) | 字符串长度 | length("hello") → 5 |
substr(s, i, n) | 子串 | substr("abcde", 2, 3) → “bcd” |
index(s, t) | 查找位置 | index("hello", "ll") → 3 |
split(s, a, sep) | 分割 | split("a:b:c", arr, ":") |
sub(r, s, t) | 替换第一个 | sub(/o/, "O", $0) |
gsub(r, s, t) | 全局替换 | gsub(/o/, "O", $0) |
match(s, r) | 正则匹配 | match($0, /[0-9]+/) |
sprintf(fmt, ...) | 格式化 | sprintf("%05d", 42) |
tolower(s) | 转小写 | tolower("HELLO") |
toupper(s) | 转大写 | toupper("hello") |
gensub(r, s, h, t) | 通用替换 (gawk) | gensub(/(.)(.)/, "\\2\\1", "g", "ab") |
数学函数
| 函数 | 说明 |
|---|---|
sin(x), cos(x), atan2(y,x) | 三角函数 |
exp(x), log(x), sqrt(x) | 指数、对数、平方根 |
int(x) | 取整 |
rand() | 0-1 随机数 |
srand(x) | 设置随机种子 |
# 生成 1-100 的随机数
awk 'BEGIN { srand(); print int(rand()*100)+1 }'
# 计算标准差
awk '{
sum += $1
sumsq += $1 * $1
n++
}
END {
mean = sum / n
variance = sumsq/n - mean*mean
printf "均值: %.2f, 标准差: %.2f\n", mean, sqrt(variance)
}' data.txt
I/O 函数
# getline — 读取额外输入
awk '{
cmd = "date +%Y-%m-%d"
cmd | getline today
close(cmd)
print today, $0
}' file
# 管道命令
awk '{
print $1 | "sort -u"
close("sort -u")
}' file
⚠️ 注意:使用管道命令后务必
close(),否则会耗尽文件描述符。
5.4 多文件处理
FNR 和 NR 的区别
# FNR — 当前文件内的行号(每读新文件重置)
# NR — 全局行号(持续递增)
$ awk '{print FILENAME, NR, FNR}' file1.txt file2.txt
→ file1.txt 1 1
→ file1.txt 2 2
→ file2.txt 3 1
→ file2.txt 4 2
🏢 业务场景:关联两个文件
# users.txt — 用户信息
cat > users.txt << 'EOF'
1001 Alice
1002 Bob
1003 Carol
EOF
# orders.txt — 订单数据
cat > orders.txt << 'EOF'
1001 500.00
1001 300.00
1002 200.00
1003 800.00
EOF
# 先加载用户信息到数组,再处理订单
$ awk '
NR==FNR { name[$1] = $2; next }
{ printf "%-10s %s %10.2f\n", name[$1], $1, $2 }
' users.txt orders.txt
→ Alice 1001 500.00
→ Alice 1001 300.00
→ Bob 1002 200.00
→ Carol 1003 800.00
💡 关键技巧:
NR==FNR只在第一个文件中为 true,配合next可以区分文件处理逻辑。
用 AWK 实现 JOIN 操作
# INNER JOIN
awk '
NR==FNR { data[$1] = $2; next }
$1 in data { print $0, data[$1] }
' file1.txt file2.txt
# LEFT JOIN(输出所有右表记录,匹配不到填默认值)
awk '
NR==FNR { data[$1] = $2; next }
{ print $0, ($1 in data ? data[$1] : "N/A") }
' file1.txt file2.txt
5.5 管道 I/O 与外部命令
将输出传给外部命令
# 将每行输出传给外部命令
awk '{print $1 | "sort"}' file
# 将所有输出一次性传给外部命令
awk '{print $1}' file | sort
# 读取外部命令输出
awk 'BEGIN {
while (("date" | getline line) > 0) {
print "当前时间:", line
}
close("date")
}'
🏢 业务场景:执行批量命令
# 对日志中出现的每个 IP 执行 whois 查询
awk '{print $1}' access.log | sort -u | while read ip; do
echo "=== $ip ==="
whois "$ip" | awk '/Organization/ {print; exit}'
done
# AWK 内部直接执行
awk '!seen[$1]++ {
cmd = "dig +short " $1
cmd | getline result
close(cmd)
print $1, "->", result
}' hostnames.txt
5.6 高级格式化输出
精美的表格输出
cat > employees.txt << 'EOF'
Alice Engineering 15000
Bob Marketing 12000
Carol Engineering 16000
Dave Sales 11000
Eve Engineering 14000
EOF
awk '
BEGIN {
width_name = 12
width_dept = 15
width_sal = 12
total_width = width_name + width_dept + width_sal + 6
# 打印上边框
printf "┌"
for(i=1; i<=width_name+2; i++) printf "─"
printf "┬"
for(i=1; i<=width_dept+2; i++) printf "─"
printf "┬"
for(i=1; i<=width_sal+2; i++) printf "─"
printf "┐\n"
# 打印表头
printf "│ %-*s │ %-*s │ %*s │\n", width_name, "姓名", width_dept, "部门", width_sal, "薪资"
# 打印分隔线
printf "├"
for(i=1; i<=width_name+2; i++) printf "─"
printf "┼"
for(i=1; i<=width_dept+2; i++) printf "─"
printf "┼"
for(i=1; i<=width_sal+2; i++) printf "─"
printf "┤\n"
}
{
printf "│ %-*s │ %-*s │ %*d │\n", width_name, $1, width_dept, $2, width_sal, $3
sum += $3
}
END {
printf "├"
for(i=1; i<=width_name+2; i++) printf "─"
printf "┼"
for(i=1; i<=width_dept+2; i++) printf "─"
printf "┼"
for(i=1; i<=width_sal+2; i++) printf "─"
printf "┤\n"
printf "│ %-*s │ %-*s │ %*d │\n", width_name, "合计", width_dept, "", width_sal, sum
printf "└"
for(i=1; i<=width_name+2; i++) printf "─"
printf "┴"
for(i=1; i<=width_dept+2; i++) printf "─"
printf "┴"
for(i=1; i<=width_sal+2; i++) printf "─"
printf "┘\n"
}' employees.txt
生成 HTML 表格
awk '
BEGIN {
print "<table border=\"1\" cellpadding=\"5\">"
print "<tr><th>Name</th><th>Department</th><th>Salary</th></tr>"
}
{
printf "<tr><td>%s</td><td>%s</td><td>%d</td></tr>\n", $1, $2, $3
}
END {
print "</table>"
}' employees.txt
生成 Markdown 表格
awk '
BEGIN {
printf "| %-10s | %-15s | %10s |\n", "Name", "Department", "Salary"
printf "|%-11s|%-16s|%-11s|\n", "-----------", "----------------", "-----------"
}
{ printf "| %-10s | %-15s | %10d |\n", $1, $2, $3 }
END {
printf "|%-11s|%-16s|%-11s|\n", "-----------", "----------------", "-----------"
}' employees.txt
5.7 BEGINFILE 和 ENDFILE(GNU AWK)
# GNU AWK 4.0+ 支持 BEGINFILE 和 ENDFILE
gawk '
BEGINFILE { print "开始处理:", FILENAME }
FNR==1 { print "--- 文件内容 ---" }
{ print }
ENDFILE { print "结束处理:", FILENAME; print "" }
' file1.txt file2.txt file3.txt
🏢 业务场景:处理多个 CSV 文件并添加文件名列
gawk -F, '
BEGINFILE { fname = FILENAME; sub(/.*\//, "", fname) }
NR > 1 { print $0 "," fname }
' *.csv
5.8 高级数组技巧
用数组模拟集合操作
# 集合去重(类似 sort -u)
awk '!seen[$0]++' file
# 两个文件的交集
awk '
NR==FNR { set[$0]; next }
$0 in set
' file1.txt file2.txt
# 两个文件的差集(在 file1 中但不在 file2 中)
awk '
NR==FNR { set[$0]; next }
!($0 in set)
' file2.txt file1.txt
# 两个文件的并集
awk '!seen[$0]++' file1.txt file2.txt
用数组实现计数器和累加器
# 复杂统计示例
awk -F, '{
dept = $2
# 初始化最大最小值
if (!(dept in max_sal) || $3 > max_sal[dept]) max_sal[dept] = $3
if (!(dept in min_sal) || $3 < min_sal[dept]) min_sal[dept] = $3
sum_sal[dept] += $3
count[dept]++
} END {
for (d in count) {
printf "%-15s 人数:%3d 平均:%8.0f 最高:%8d 最低:%8d\n",
d, count[d], sum_sal[d]/count[d], max_sal[d], min_sal[d]
}
}' employees.csv
数组排序输出
# GNU AWK 的 PROCINFO["sorted_in"]
awk 'BEGIN {
PROCINFO["sorted_in"] = "@val_num_desc"
}
{ count[$1]++ }
END {
for (ip in count) printf "%8d %s\n", count[ip], ip
}' access.log
# 其他排序方式
# @ind_str_asc — 按字符串下标升序
# @ind_num_asc — 按数值下标升序
# @val_str_asc — 按字符串值升序
# @val_num_asc — 按数值值升序
# @val_num_desc — 按数值值降序
5.9 COPROC 和网络编程(GNU AWK)
# 使用协进程(coprocess)
awk 'BEGIN {
while (("date" |& getline line) > 0) {
print line
}
close("date", "to")
}'
💡 扩展:GNU AWK 还支持网络编程(
/inet/tcp/...),但这超出了本教程的范围。感兴趣的读者可以查阅 GNU AWK 手册。
5.10 综合实战
🏢 场景一:日志分析仪表板
cat > report.awk << 'EOF'
BEGIN {
print "========================================"
print " 访问日志分析报告"
print "========================================"
total = 0
errors = 0
}
{
total++
ip_count[$1]++
status_count[$9]++
path_count[$7]++
bytes += $10
if ($9 ~ /^[45]/) errors++
if ($9 ~ /^404$/) not_found++
}
END {
print ""
printf "总请求数: %d\n", total
printf "错误请求数: %d (%.1f%%)\n", errors, errors/total*100
printf "404 请求数: %d\n", not_found
printf "总传输量: %.2f MB\n", bytes/1048576
print ""
print "--- Top 10 IP ---"
# 排序输出(使用外部 sort)
for (ip in ip_count) {
printf "%8d %s\n", ip_count[ip], ip
} | "sort -rn | head -10"
close("sort -rn | head -10")
print ""
print "--- 状态码分布 ---"
for (s in status_count) {
printf "%-6s %6d (%.1f%%)\n", s, status_count[s], status_count[s]/total*100
}
}
EOF
$ awk -f report.awk access.log
🏢 场景二:实时监控脚本
#!/bin/bash
# monitor.sh — 实时监控日志
tail -f /var/log/nginx/access.log | awk '
BEGIN {
print "开始监控..."
errors = 0
total = 0
}
{
total++
if ($9 ~ /^[45]/) {
errors++
printf "\033[31m[警告] %s %s %s\033[0m\n", $1, $7, $9
}
if (total % 100 == 0) {
printf "[统计] 总请求: %d, 错误: %d (%.1f%%)\n",
total, errors, (errors/total)*100
}
}'
5.11 AWK 进阶速查
# 数组
arr[key] = value # 赋值
key in arr # 检查存在
delete arr[key] # 删除元素
for (k in arr) ... # 遍历
length(arr) # 元素个数 (gawk)
# 函数
function name(params, locals) { ... }
function abs(x) { return (x < 0) ? -x : x }
function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s }
# 内置函数
length(s) substr(s,i,n) index(s,t) split(s,a,sep)
sub(r,s,t) gsub(r,s,t) match(s,r) sprintf(fmt,...)
tolower(s) toupper(s) int(x) sqrt(x)
# 多文件
NR==FNR { ...; next } # 只在第一个文件执行
FNR==1 { ... } # 每个文件的第一行
FILENAME # 当前文件名
# 排序输出
for (k in arr) print k, arr[k] | "sort"
扩展阅读
- GNU AWK — Advanced Features
- Effective AWK Programming — Arnold Robbins
- AWK Array Sorting with PROCINFO
下一章:第 6 章:正则表达式 — 深入理解 BRE、ERE 和 PCRE。