第 42 课:awk 高级用法与数组
学习目标
完成本课学习后,你将能够:
- 理解并熟练使用awk的关联数组进行复杂的数据聚合与统计。
- 掌握模拟和处理多维数组的技巧。
- 熟练运用awk的内置函数(
split,length,tolower等)与自定义函数来增强脚本逻辑。 - 结合循环、条件语句与数组,解决实际的文本数据处理问题(如报表生成、日志分析)。
- 了解并避免在使用awk数组时常见的陷阱。
核心概念
在上一课中,我们学习了awk的基本结构和行处理。本课我们将深入其最强大的特性之一:数组。在awk中,数组与其他编程语言(如C/Python)的数组有本质区别。
-
关联数组:awk中所有的数组都是关联数组。这意味着它的下标(索引)不仅可以是数字,更可以是任意字符串。你可以把它想象成一个“字典”或“哈希表”,用于建立键值对(
key-value)关系。# 这是完全合法的awk数组操作 fruit[“apple”] = 1 fruit[“orange”] = 2 count[1] = “first” -
数组的隐式创建与自动初始化:在awk中,当你给一个数组元素赋值时,如果数组或该元素不存在,它会被自动创建。访问一个不存在的数组元素会得到一个空字符串(在算术上下文中会被视为0)。
# 不需要提前声明 `my_array` print my_array[“key”] # 输出空字符串 my_array[“key”] = “hello” # 数组被创建并赋值 -
遍历数组:使用
for (key in array)循环来遍历数组的所有索引(注意:是索引,不是值)。顺序是未定义的,不能假设。for (name in scores) { print name, scores[name] } -
模拟多维数组:awk不直接支持多维数组,但可以通过组合索引来模拟。例如,用下标
"i,j"来表示第i行第j列。matrix[“1,1”] = 10 matrix[“1,2”] = 20 matrix[“2,1”] = 30 -
删除数组元素:使用
delete array[key]或delete array来删除单个元素或整个数组。delete scores[“Tom”] # 删除Tom的成绩 delete all_scores # 删除整个all_scores数组
代码示例
示例1:使用关联数组统计单词出现次数
这是一个经典的数组应用,统计文本中每个单词出现的次数。
#!/bin/bash
# 文件: word_count.awk
# 用法: echo "hello world hello awk" | awk -f word_count.awk
# 对每一行进行处理
{
# `for (i=1; i<=NF; i++)` 遍历当前行的每一个单词
for (i = 1; i <= NF; i++) {
word = $i
# 将单词作为数组索引,并自增其计数值
word_count[word]++
}
}
# 处理完所有行后,在END块中输出结果
END {
for (word in word_count) {
printf "%-15s : %d\n", word, word_count[word]
}
}
运行与结果:
$ echo "hello world hello awk and awk hello" | awk -f word_count.awk
awk : 2
and : 1
world : 1
hello : 3
示例2:处理多维数据(计算学生总分与平均分)
假设我们有一个学生成绩数据文件 scores.txt,格式如下:
Tom 85 90 78
Jerry 92 88 95
Alice 76 82 90
#!/bin/bash
# 文件: student_score.awk
# 用法: awk -f student_score.awk scores.txt
# 处理每行学生数据
{
name = $1
# 模拟一个二维数组: student_scores[学生名,第几科]
# 或者更简单,用一个一维数组存储总分,另一个存储科目数
total_score[name] = 0
num_subjects = 0
for (i = 2; i <= NF; i++) {
total_score[name] += $i
num_subjects++
}
# 存储每个学生的科目数(假设所有人科目数相同,这里仅作示例)
subject_count[name] = num_subjects
}
# 所有行处理完后,输出报表
END {
printf "%-10s %-8s %-8s\n", "Name", "Total", "Average"
printf "%-10s %-8s %-8s\n", "----", "-----", "-------"
for (student in total_score) {
total = total_score[student]
avg = total / subject_count[student]
printf "%-10s %-8d %-8.2f\n", student, total, avg
}
}
运行与结果:
$ awk -f student_score.awk scores.txt
Name Total Average
---- ----- -------
Tom 253 84.33
Jerry 275 91.67
Alice 248 82.67
示例3:使用内置函数与自定义函数处理日志
假设我们有一个Nginx访问日志,想统计每个IP的请求次数,并找出请求次数最多的IP。
#!/bin/bash
# 文件: ip_analysis.awk
# 用法: awk -f ip_analysis.awk access.log
# 自定义函数:返回两个数中的较大值
function max(a, b) {
return (a > b) ? a : b
}
# 主处理块:统计每个IP的出现次数
{
# 假设日志格式为: 192.168.1.1 - - [10/Oct/2023:13:55:36 +0800] "GET /index.html HTTP/1.1" 200 612
ip = $1
ip_count[ip]++
# 实时记录当前遇到的最大请求数和对应IP
if (ip_count[ip] > max_count) {
max_count = ip_count[ip]
max_ip = ip
}
}
END {
print "=== IP Request Statistics ==="
for (ip in ip_count) {
print ip ": " ip_count[ip] " requests"
}
print "============================="
print "Most Active IP: " max_ip " with " max_count " requests"
}
实践练习
-
基础练习:词频统计并排序 编写一个awk脚本,统计一个文本文件(如
test.txt)中所有单词的出现频率,并按频率从高到低排序输出。提示:在END块中使用PROCINFO["sorted_in"] = "@val_num_desc"来设置排序方式。预期输入文件
test.txt内容:apple banana apple orange banana apple grape orange grape banana apple预期输出示例(顺序应固定):
apple: 4 banana: 3 orange: 2 grape: 2 -
进阶练习:处理CSV数据 给定一个CSV文件
sales.csv,内容如下:Date,Product,Region,Amount 2023-01-01,Phone,North,500 2023-01-01,Tablet,South,300 2023-01-02,Phone,South,400 2023-01-02,Laptop,North,800 2023-01-01,Phone,North,200 2023-01-02,Laptop,South,600编写awk脚本,统计每个Region(区域)的总销售额,并输出。注意:第一行是标题,需要跳过(使用
NR>1)。预期输出:
North: 1500 South: 1300 -
综合练习:二维数据求和 创建一个awk脚本,模拟一个简单的“学生成绩单计算器”。输入是一个包含学生姓名和多门科目成绩的文本文件(类似示例2)。除了计算总分和平均分,请增加一项:找出每科成绩的最高分。
输入文件同示例2的
scores.txt,预期输出需增加:Subject Max Scores: Subject 1: 92 Subject 2: 90 Subject 3: 95
常见错误
-
误以为awk数组是基于整数索引的连续数组:这是最常见的误区。忘记
for (key in array)中key是字符串,而试图进行数值比较或排序时可能会得到意外结果。- 错误示例:
if (key > 10),这里的key是字符串,比较的是字典序,而不是数值。 - 正确做法:如果需要数值排序,使用
PROCINFO["sorted_in"]或先处理数据。
- 错误示例:
-
遍历数组的顺序不可靠:不要假设
for (key in array)的遍历顺序是插入顺序或任何特定顺序。如果需要有序输出,必须额外处理(如将索引存入另一个数组并排序)。 -
在函数中修改全局数组:在自定义函数中直接修改通过参数传递的数组(实际上awk只按值传递普通变量)可能不会改变原数组。要传递数组,需要将其作为函数的额外参数。
function modify_array(arr, key, value) { # 这里的arr是原数组的一个引用 arr[key] = value } # 调用时,需要把数组名作为参数传递 modify_array(my_array, “new_key”, 100) -
使用
length(array)获取数组长度:在标准的awk(如gawk的某些模式)中,length(array)可能返回未定义行为。要计算数组元素个数,更可靠的做法是遍历计数。count = 0 for (i in array) count++
小结
本课我们探索了awk最强大的特性——数组。关键要点如下:
- 关联数组是核心:awk的数组本质是键值对集合,索引可以是任意字符串,这使其在数据聚合(如统计、分组)方面极其灵活。
- 模拟多维结构:通过组合索引(如
"i,j")可以模拟二维甚至更高维的数组,用于处理表格数据。 - 函数增强能力:结合内置函数(
split,tolower)和自定义函数,可以构建复杂的文本处理流水线。 - 注意陷阱:牢记数组索引是字符串、遍历顺序不定、正确传递数组给函数。
- 实战应用:数组是日志分析、报表生成、数据清洗等运维脚本中的核心工具。
掌握awk的数组,将极大提升你处理结构化和半结构化文本数据的效率。在下一课中,我们将学习另一个强大的流编辑器 sed。