0Pricing
Linux Command Line & Bash Scripting Mastery · 课时

使用 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 工程中的惯用做法:

  1. awk 将出现次数统计到数组中,并在 END 中输出 count key
  2. sort -rn 按数值降序排序
  3. 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. awk 中的记录、字段和自定义分隔符
  2. 模式、范围与 BEGIN/END 块
  3. 使用 awk 数组进行聚合与分组
  4. awk 函数、printf 格式化与报表生成
← 返回 Linux Command Line & Bash Scripting Mastery