42·Shell 脚本进阶高级

awk 高级用法与数组

awkarrays

第 42 课:awk 高级用法与数组

学习目标

完成本课学习后,你将能够:

  1. 理解并熟练使用awk的关联数组进行复杂的数据聚合与统计。
  2. 掌握模拟和处理多维数组的技巧。
  3. 熟练运用awk的内置函数(split, length, tolower等)与自定义函数来增强脚本逻辑。
  4. 结合循环、条件语句与数组,解决实际的文本数据处理问题(如报表生成、日志分析)。
  5. 了解并避免在使用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"
}

实践练习

  1. 基础练习:词频统计并排序 编写一个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
    
  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
    
  3. 综合练习:二维数据求和 创建一个awk脚本,模拟一个简单的“学生成绩单计算器”。输入是一个包含学生姓名和多门科目成绩的文本文件(类似示例2)。除了计算总分和平均分,请增加一项:找出每科成绩的最高分

    输入文件同示例2的 scores.txt,预期输出需增加:

    Subject Max Scores:
    Subject 1: 92
    Subject 2: 90
    Subject 3: 95
    

常见错误

  1. 误以为awk数组是基于整数索引的连续数组:这是最常见的误区。忘记 for (key in array)key 是字符串,而试图进行数值比较或排序时可能会得到意外结果。

    • 错误示例if (key > 10),这里的 key 是字符串,比较的是字典序,而不是数值。
    • 正确做法:如果需要数值排序,使用 PROCINFO["sorted_in"] 或先处理数据。
  2. 遍历数组的顺序不可靠:不要假设 for (key in array) 的遍历顺序是插入顺序或任何特定顺序。如果需要有序输出,必须额外处理(如将索引存入另一个数组并排序)。

  3. 在函数中修改全局数组:在自定义函数中直接修改通过参数传递的数组(实际上awk只按值传递普通变量)可能不会改变原数组。要传递数组,需要将其作为函数的额外参数

    function modify_array(arr, key, value) {
        # 这里的arr是原数组的一个引用
        arr[key] = value
    }
    # 调用时,需要把数组名作为参数传递
    modify_array(my_array, “new_key”, 100)
    
  4. 使用 length(array) 获取数组长度:在标准的awk(如gawk的某些模式)中,length(array) 可能返回未定义行为。要计算数组元素个数,更可靠的做法是遍历计数。

    count = 0
    for (i in array) count++
    

小结

本课我们探索了awk最强大的特性——数组。关键要点如下:

  • 关联数组是核心:awk的数组本质是键值对集合,索引可以是任意字符串,这使其在数据聚合(如统计、分组)方面极其灵活。
  • 模拟多维结构:通过组合索引(如 "i,j")可以模拟二维甚至更高维的数组,用于处理表格数据。
  • 函数增强能力:结合内置函数(split, tolower)和自定义函数,可以构建复杂的文本处理流水线。
  • 注意陷阱:牢记数组索引是字符串、遍历顺序不定、正确传递数组给函数。
  • 实战应用:数组是日志分析、报表生成、数据清洗等运维脚本中的核心工具。

掌握awk的数组,将极大提升你处理结构化和半结构化文本数据的效率。在下一课中,我们将学习另一个强大的流编辑器 sed

练习编辑器

rust
Loading...

继续学习

完成本课后,建议继续学习下一课「sed 流编辑器基础」 以巩固所学知识。