使用 awk 数组进行聚合与分组
使用以字段值为键的关联数组计算总和、计数和分组汇总。
使用 awk 数组进行聚合与分组 是 CoddyKit 上的免费 Linux Command Line & Bash Scripting Mastery 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Linux Command Line & Bash Scripting Mastery 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
什么是 awk 关联数组?
在 awk 中,关联数组是一种键值存储,其中的键可以是任意字符串或数字。与大多数语言中的索引数组不同,awk 数组在底层是哈希映射,非常适合按照字段值对数据进行分组和聚合。
- 隐式声明:只需赋值
arr[key] = value - 键默认为字符串(数字会被强制转换)
- 没有大小限制——awk 会根据需要扩展数组
- 非常适合在单次遍历中计算总和、计数和分组汇总
递增某个键之前不需要先初始化它——对于未设置的键,awk 会自动将其视为零或空字符串。
按组统计行数
最常见的聚合模式,是统计字段中的每个唯一值出现了多少次。使用字段 $1(或任意字段)作为数组键,并在每个匹配的行上递增计数器。
所有输入都处理完后,END 块才会运行——您可以在那里输出累计结果。
count[$1]++处理完成后,count 保存了 $1 每个唯一值对应的行数。
#!/usr/bin/env bash
# Count how many log entries exist per HTTP status code
# Input format: <ip> <date> <method> <path> <status> <bytes>
printf '10.0.0.1 2024-01-01 GET /index 200 512
10.0.0.2 2024-01-01 POST /api 404 128
10.0.0.3 2024-01-01 GET /img 200 2048
10.0.0.4 2024-01-01 GET /api 500 64
10.0.0.5 2024-01-01 DELETE /api 404 32
' | awk '
{
count[$5]++
}
END {
for (status in count)
print status, count[status]
}'
按组求数值字段的和
计数只是聚合的一种形式。若要按照另一个字段对数值字段进行求和,请将数值累加到以分组字段为键的数组中。
模式如下:
- 键 = 分组字段(例如,用户名对应的
$1) - 值 = 数值字段的累计总和(例如,字节数对应的
$2)
这样只需遍历一次 awk,就能完成完整的分组求和——无需排序或预处理。
#!/usr/bin/env bash
# Sum bytes transferred per user from an access log
printf 'alice 1024
bob 512
alice 2048
charlie 256
bob 768
alice 128
' | awk '
{
bytes[$1] += $2
}
END {
for (user in bytes)
printf "%-10s %d bytes\n", user, bytes[user]
}'
一次遍历同时计算计数和总和
awk 允许您同时维护多个数组,因此只需扫描一次文件,就能为每个组计算计数和总和(进而计算平均值)。这比运行两次管道高效得多。
count[key]++——跟踪出现次数total[key] += $N——跟踪累计总和- 在
END中,将total[k] / count[k]相除即可得到每组的平均值
#!/usr/bin/env bash
# Compute per-department headcount and average salary
printf 'Engineering Alice 95000
Engineering Bob 88000
Marketing Carol 72000
Marketing Dave 68000
Engineering Eve 102000
Marketing Frank 75000
' | awk '
{
dept = $1
sal = $3
count[dept]++
total[dept] += sal
}
END {
printf "%-15s %5s %10s\n", "Department", "Count", "Avg Salary"
for (d in count)
printf "%-15s %5d %10.0f\n", d, count[d], total[d]/count[d]
}'
使用多字段键进行二维分组
您可以使用分隔符连接多个字段,从而创建复合键。这样无需特殊语法即可实现二维分组。
请选择一个不会出现在数据中的分隔符(例如 SUBSEP、awk 内置的记录分隔符 \034,或字面量竖线 |)。
- 复合键:
arr[$1 SUBSEP $2]或arr[$1"|"$2] - 打印时将键重新拆分:
split(key, parts, "|")
#!/usr/bin/env bash
# Count requests grouped by HTTP method AND status code
printf 'GET 200
POST 201
GET 404
GET 200
DELETE 204
POST 201
GET 404
POST 500
' | awk '
{
key = $1 "|" $2
count[key]++
}
END {
printf "%-8s %-6s %s\n", "Method", "Status", "Count"
for (k in count) {
split(k, parts, "|")
printf "%-8s %-6s %d\n", parts[1], parts[2], count[k]
}
}'
跟踪每组的最小值和最大值
关联数组可以轻松跟踪每组的最小值和最大值。诀窍是使用特殊条件 !(key in arr),仅在每个键第一次出现时进行初始化。
- 某个键第一次出现时,
!(key in min_arr)的结果为真 - 初始化后,使用标准的小于或大于检查进行比较并覆盖
这种模式可以避免无意义的零值破坏最小值。
#!/usr/bin/env bash
# Find min and max response time per endpoint
printf '/api/users 120
/api/orders 340
/api/users 98
/api/orders 512
/api/users 210
/api/orders 280
/health 5
/health 7
' | awk '
{
ep = $1
rt = $2
if (!(ep in mn)) { mn[ep] = rt; mx[ep] = rt }
if (rt < mn[ep]) mn[ep] = rt
if (rt > mx[ep]) mx[ep] = rt
}
END {
printf "%-15s %6s %6s\n", "Endpoint", "Min", "Max"
for (ep in mn)
printf "%-15s %6d %6d\n", ep, mn[ep], mx[ep]
}'
频数分布——前 N 个组
一个常见的实际任务是找出出现频率最高的前 N 个值。awk 负责计数;通过管道交给 sort 和 head,即可完成排序和截取。
这种管道模式是 shell 工程中的惯用做法:
- awk 将出现次数统计到数组中,并在
END中输出count key sort -rn按数值降序排序head -n 5只保留前 5 项
让 awk 专注于聚合,并将排序交给 sort,符合 Unix 哲学。
#!/usr/bin/env bash
# Top 3 most active IP addresses from an access log
printf '192.168.1.10 GET /index
10.0.0.5 POST /api
192.168.1.10 GET /css
172.16.0.3 GET /index
10.0.0.5 GET /api
192.168.1.10 DELETE /api
172.16.0.3 POST /login
10.0.0.5 GET /index
10.0.0.5 POST /logout
' | awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' \
| sort -rn \
| head -n 3
使用 NR 和 FNR 进行多文件聚合
处理多个文件时,awk 的内置变量 NR(已读取的记录总数)和 FNR(当前文件中的记录数)配合 FILENAME,可以标记每条记录来自哪个文件。
FILENAME——当前正在读取的文件名FNR == 1——在每个新文件开始时触发(适合跳过标题行)
这样,只需调用一次 awk,就能跨整个日志目录进行按文件分组的聚合。
从 awk 数组输出排序后的结果
awk 中的 for (key in array) 循环不保证顺序。若要获得确定性的输出,可以将 awk 在 END 中的打印结果通过管道交给 sort;也可以使用 asorti / asort 函数在 awk 内收集并排序值(仅限 GNU awk)。
为了编写跨平台脚本,通常更推荐可移植的 shell 管道方式:
sort -k1,1——按第一个字段(分组键)按字母顺序排序sort -k2,2rn——按第二个字段(计数/总和)按数值降序排序
#!/usr/bin/env bash
# Bytes per user, sorted alphabetically by username
printf 'zara 800
alice 1500
bob 300
alice 200
zara 400
bob 1100
' | awk '
{
total[$1] += $2
}
END {
for (u in total)
print u, total[u]
}' | sort -k1,1
删除数组键并清除状态
有时您需要在数据流中途重置聚合状态——例如,处理日志文件时,每个部分由空行或哨兵值分隔。
delete arr[key]——删除单个条目delete arr——清除整个数组(GNU awk)- 访问前使用
(key in arr)检查是否存在,以免创建幽灵条目
这种技术无需重启 awk,即可实现滑动窗口或按部分聚合。
#!/usr/bin/env bash
# Sum values within each section delimited by "---"
printf 'alice 100
bob 200
---
alice 50
bob 75
charlie 300
---
' | awk '
/^---/ {
print "-- Section totals --"
for (u in total) printf " %-10s %d\n", u, total[u]
delete total
next
}
{
total[$1] += $2
}'
实际管道:Nginx 日志摘要
下面将这些内容整合起来——这是一个针对 Nginx 组合日志格式的生产级 awk 单次遍历聚合示例。它只需扫描一次,就能计算每个虚拟主机的请求数、总字节数和错误率。
使用的关键技术:
- 复合键:从某个字段中提取
vhost - 并行数组:
reqs[]、bytes[]、errors[] - 条件累加:使用
$9 >= 400仅统计错误响应 - 在
END中使用格式化的printf表格
#!/usr/bin/env bash
# Simulated Nginx combined log: host ip - - [date] "METHOD /path HTTP/1.1" status bytes
printf 'api.example.com 10.0.0.1 - - [01/Jan/2024] "GET /v1" 200 1024
www.example.com 10.0.0.2 - - [01/Jan/2024] "GET /" 200 4096
api.example.com 10.0.0.3 - - [01/Jan/2024] "POST /v1" 500 128
www.example.com 10.0.0.4 - - [01/Jan/2024] "GET /img" 404 64
api.example.com 10.0.0.5 - - [01/Jan/2024] "GET /v1" 200 2048
www.example.com 10.0.0.6 - - [01/Jan/2024] "GET /" 200 4096
api.example.com 10.0.0.7 - - [01/Jan/2024] "DELETE /v1" 403 32
' | awk '
{
host = $1; status = $9; b = $10
reqs[host]++
bytes[host] += b
if (status + 0 >= 400) errors[host]++
}
END {
printf "%-20s %6s %10s %6s\n", "Host", "Reqs", "Bytes", "Errors"
for (h in reqs)
printf "%-20s %6d %10d %6d\n", h, reqs[h], bytes[h], errors[h]+0
}'
知识检查:正确初始化最小值
测试您对 awk 聚合中一个常见陷阱的理解。
课程回顾:使用 awk 数组进行聚合
您已经学习了完全在 awk 中计算分组聚合的核心模式:
- 计数:
count[$key]++——每行递增一次,在END中打印 - 求和:
total[$key] += $N——按组累加数值字段 - 平均值:同时维护
count[]和total[],在END中相除 - 最小值/最大值:使用
!(key in arr)在第一次出现时初始化,然后进行比较 - 复合键:使用分隔符连接多个字段,实现多维分组
- 排序输出:将 awk 在
END中的打印结果通过管道交给sort,以获得确定性的顺序 - 部分重置:使用
delete arr清除输入中不同逻辑部分之间的状态
这些模式让您可以用一次高效的 awk 调用,替代复杂的数据库查询或多次管道遍历——这是生产环境 shell 工程的一项必备技能。
常见问题解答
「使用 awk 数组进行聚合与分组」课时是免费的吗?
是的 — 「使用 awk 数组进行聚合与分组」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Linux Command Line & Bash Scripting Mastery 课程的其余内容,请升级到 CoddyKit PRO。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
「使用 awk 数组进行聚合与分组」这节课中我会学到什么?
使用以字段值为键的关联数组计算总和、计数和分组汇总。 你通过在浏览器中直接运行的动手代码来练习 Linux Command Line & Bash Scripting Mastery,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Linux Command Line & Bash Scripting Mastery 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Linux Command Line & Bash Scripting Mastery 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「使用 awk 数组进行聚合与分组」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Linux Command Line & Bash Scripting Mastery 课中编写并运行代码吗?
能。每节 Linux Command Line & Bash Scripting Mastery 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- awk 中的记录、字段和自定义分隔符
- 模式、范围与 BEGIN/END 块
- 使用 awk 数组进行聚合与分组
- awk 函数、printf 格式化与报表生成