awk 从入门到进阶(下)

前言

回顾精华,承前启后:

在前两篇中,我们已逐步探索了 awk 的强大功能,从基础的字段提取,到中级的条件判断与循环控制,相信你已对其核心能力有了深入的理解。然而,awk 的潜力远不止于此。在本篇(下篇)中,我们将继续深入 awk 的高级特性与脚本化应用,助你从熟练走向精通。

本篇我们将聚焦以下核心内容:

  • 关联数组的精妙应用: 深入探索 awk 的哈希表功能,掌握高效的数据统计与分组技巧。
  • 多文件处理的艺术: 运用 FNRNR 变量,轻松应对多文件输入,解决复杂的数据合并与分析需求。
  • 脚本编写的进阶之道: 从命令行到独立脚本,结合参数传递(如 -v),实现 awk 脚本的模块化与复用。
  • 高级技巧的深度剖析: 介绍 $1=$1去除多余空白等高级技巧,提升文本处理的精度与效率。

通过本篇的学习,你将能够:

  • awk 应用于更复杂的实际场景,如日志分析中的模式统计、多文件数据对比等。
  • 掌握 awk 脚本的开发与调试,实现文本处理与数据分析的自动化。
  • 深入理解 awk 的高级特性,在日常工作中更加得心应手。

让我们一同开启 awk 高级功能的探索之旅,迈向文本处理与数据分析的更高境界吧!


关联数组:高效的数据统计与分组

awk 的关联数组(也称为哈希表)是其高级特性之一,允许以键值对的形式存储和操作数据。与传统数组使用数字索引不同,关联数组的索引可以是任意字符串,这使其在统计、计数和分组任务中尤为强大。

基本概念

awk 中,关联数组无需预先声明,直接通过赋值创建。例如:

array[key] = value
  • key:可以是字符串、数字或表达式。
  • value:存储的值,通常是数字或字符串。

示例 1:统计单词出现次数

假设文件 words.txt 内容如下:

apple banana apple
orange apple banana

统计每个单词的出现次数:

awk '{for (i = 1; i <= NF; i++) count[$i]++} END {for (word in count) print word, count[word]}' words.txt

输出:

apple 3
banana 2
orange 1

解析:

  • {for (i = 1; i <= NF; i++) count[$i]++}:遍历每行字段,count[$i] 以字段值为键,累加出现次数。
  • END {for (word in count) print word, count[word]}:遍历数组,输出键(单词)和值(次数)。

示例 2:按字段分组求和

假设 sales.txt 内容如下:

apple 10
banana 20
apple 15
banana 5

按第一列分组,计算第二列总和:

awk '{sum[$1] += $2} END {for (item in sum) print item, sum[item]}' sales.txt

输出:

apple 25
banana 25

解析:

  • sum[$1] += $2:以第 1 列为键,累加第 2 列值。
  • for (item in sum):遍历数组,输出结果。

示例 3:多键组合

统计日志中 IP 和状态码的组合出现次数,假设 log.txt

192.168.1.1 200
10.0.0.1 404
192.168.1.1 200

命令:

awk '{key = $1 " " $2; count[key]++} END {for (k in count) print k, count[k]}' log.txt

输出:

192.168.1.1 200 2
10.0.0.1 404 1

解析:

  • key = $1 " " $2:用空格连接 IP 和状态码作为键。
  • count[key]++:计数组合出现次数。

注意事项

  1. 未初始化值
    未赋值的数组元素默认为 0 或空字符串。例如:

    awk 'BEGIN {print count["apple"]}'  # 输出 0
    
  2. 删除元素
    使用 delete 删除数组元素:

    awk 'BEGIN {count["apple"] = 1; delete count["apple"]; print count["apple"]}'  # 输出 0
    
  3. 遍历顺序
    for (key in array) 的遍历顺序不保证与输入顺序一致。

关联数组的强大之处在于其灵活性和高效性,特别适合日志分析中的模式统计、数据去重和分组汇总。


多文件处理:FNR 与 NR 的妙用

在处理多个输入文件时,awk 的内置变量 NRNumber of Records,全局行号)和 FNRFile Number of Records,当前文件行号)非常有用。通过它们,我们可以实现文件间的数据对比、合并和条件处理,极大地提升数据分析的效率和灵活性。

基本概念

  • NR (Number of Records):从第一个文件开始的全局行号,持续递增,表示已经读取的总行数。
  • FNR (File Number of Records):当前文件的行号,每处理新文件时重置为 1,表示当前文件中已读取的行数。

示例 1:区分单文件与多文件行号

单文件 file1.txt

line1
line2

命令:

awk '{print NR, FNR, $0}' file1.txt

输出:

1 1 line1
2 2 line2

多文件 file1.txtfile2.txtfile2.txt 内容为 line3):

awk '{print NR, FNR, $0}' file1.txt file2.txt

输出:

1 1 line1
2 2 line2
3 1 line3

解析:

  • 在处理单个文件时,NRFNR 的值始终相等。
  • 在处理多个文件时,NR 持续递增,而 FNR 在开始处理新文件时重置为 1。

示例 2:合并文件并标记来源

假设 file1.txtfile2.txt 内容如上。

命令:

awk '{print (FNR == NR ? "file1" : "file2"), $0}' file1.txt file2.txt

输出:

file1 line1
file1 line2
file2 line3

解析:

  • FNR == NR:这个条件仅在处理第一个文件时为真,因此可以用来区分不同文件的来源。

示例 3:查找文件差异

比较两个文件,找出仅在 file2.txt 中出现的行。

file1.txt

apple
banana

file2.txt

banana
orange

命令:

awk 'NR == FNR {a[$0]++; next} !a[$0]' file1.txt file2.txt

输出:

orange

解析:

  • NR == FNR {a[$0]++; next}:在处理第一个文件时,将每行作为键存入关联数组 a,并使用 next 跳过后续处理。
  • !a[$0]:在处理第二个文件时,检查每行是否作为键存在于数组 a 中,如果不存在,则打印该行。

示例 4:多文件统计

统计所有文件中第 1 列的总和。

命令:

awk '{sum += $1} END {print sum}' file1.txt file2.txt

高级案例:多文件数据关联与聚合

假设我们有两个文件,sales.txt 包含销售记录,products.txt 包含产品信息,我们需要根据产品 ID 将两个文件关联起来,并统计每个产品的总销售额。

sales.txt

101,2,10.5
102,1,20.0
101,3,12.5
103,1,30.0

products.txt

101,apple
102,banana
103,orange

命令:

awk -F',' 'NR==FNR{products[$1]=$2;next}{sum[$1]+=$3}END{for(id in sum){print products[id],sum[id]}}' products.txt sales.txt

输出:

apple 23
banana 20
orange 30

解析:

  • -F',':设置字段分隔符为逗号。
  • NR==FNR{products[$1]=$2;next}:处理 products.txt 时,将产品 ID 和名称存入关联数组 products
  • {sum[$1]+=$3}:处理 sales.txt 时,根据产品 ID 累加销售额。
  • END{for(id in sum){print products[id],sum[id]}}:在 END 块中,遍历 sum 数组,输出产品名称和总销售额。

通过 NRFNR 的灵活运用,我们可以轻松实现多文件的数据关联、聚合和分析,充分发挥 awk 在文本处理方面的强大能力。


AWK 脚本编写:命令行到模块化

awk 的使用中,随着任务复杂度的增加,单行命令可能会变得冗长且难以维护。这时,将 awk 代码嵌入 Bash 脚本或独立为 awk 脚本文件,不仅能提升代码的可读性和复用性,还能通过参数传递实现动态化处理。本节将从简单的命令行用法入手,逐步扩展到复杂的模块化脚本设计,助你全面掌握 awk 的脚本化能力。


区分 Bash 参数与 AWK 字段

在深入脚本编写之前,我们需要理解 $1$2 在不同上下文中的不同含义:

  • 在 Bash 中$1$2 表示传递给 Bash 脚本的命令行参数。例如,运行 bash script.sh 10 20$110$220
  • awk$1$2 表示当前行的字段,基于输入数据的分隔符(默认空格或制表符)分割。例如,对于输入行 apple 10 orange$1apple$210
示例 1:简单的 Bash 脚本调用 awk

假设我们需要计算两个数的平均值,可以将以下命令保存为 Bash 脚本:

创建 avg.sh

!/bin/bash
awk -v a=$1 -v b=$2 '{print (a + b) / 2}'

运行:

chmod +x avg.sh
./avg.sh 10 20

输出:

15

解析:

  • Bash 上下文
    • $1$2 是命令行参数,分别为 1020
    • -v a=$1 将 Bash 的 $110)赋值给 awk 的变量 a
    • -v b=$2 将 Bash 的 $220)赋值给 awk 的变量 b
  • AWK 上下文
    • {print (a + b) / 2} 使用变量 ab 计算平均值。
    • 此处未涉及输入文件,因此 awk$1$2(字段引用)未被使用。

嵌入 Bash 脚本:结合文件输入

现在,我们将 awk 嵌入 Bash 脚本,并结合文件输入,进一步区分 Bash 参数和 awk 字段。

示例 2:筛选文件中的高值行

创建 filter.sh

!/bin/bash
if [ -z "$1" ]; then
    echo "Error: Please provide a threshold value"
    exit 1
fi
awk -v threshold=$1 '{if ($2 > threshold) print $0}' data.txt

运行:

chmod +x filter.sh
./filter.sh 80

假设 data.txt 内容为:

item1 85 10
item2 75 20
item3 90 15

输出:

item1 85 10
item3 90 15

解析:

  • Bash 上下文
    • $1 是命令行参数 80,通过 -v threshold=$1 传递给 awk
    • [ -z "$1" ] 检查是否提供了参数,提升脚本健壮性。
  • AWK 上下文
    • thresholdawk 中的变量,值为 80
    • $2 是每行的第 2 个字段(如 857590)。
    • {if ($2 > threshold) print $0} 筛选第 2 列大于 80 的行。

这里,Bash 的 $1(参数)通过 -v 转换为 awkthreshold,而 awk$2 表示字段,两者含义完全不同。


独立 AWK 脚本:从 Bash 到模块化

当逻辑复杂或需要复用时,将 awk 代码独立为脚本文件是一个更好的选择。我们将从简单脚本开始,逐步扩展功能。

示例 3:基本的独立 awk 脚本

创建 simple.awk

!/usr/bin/awk -f
BEGIN {
    print "Start processing..."
}
{
    print $1, $2
}
END {
    print "Done."
}

运行:

chmod +x simple.awk
./simple.awk data.txt

输出:

Start processing...
item1 85
item2 75
item3 90
Done.

解析:

  • #!/usr/bin/awk -f:指定 awk 为解释器,-f 表示从文件读取。
  • $1, $2:直接引用输入文件的第 1 和第 2 个字段。
  • 无需 Bash 参数,专注于文件处理。
示例 4:带参数的 awk 脚本

将 Bash 示例中的平均值计算改为独立脚本,创建 avg.awk

!/usr/bin/awk -f
BEGIN {
    if (a == "" || b == "") {
        print "Error: Please provide two numbers using -v a=NUM -v b=NUM"
        exit 1
    }
    print "Average of", a, "and", b, "is:", (a + b) / 2
}

运行:

chmod +x avg.awk
./avg.awk -v a=10 -v b=20

输出:

Average of 10 and 20 is: 15

解析:

  • -v a=10 -v b=20:直接在命令行传递参数给 awk
  • BEGIN 块:无需输入文件,专注于计算。
  • 与 Bash 脚本不同,这里无需依赖 Shell 的 $1$2,参数直接由 -v 提供。

复杂脚本:模块化设计与字段处理

当任务涉及多步骤处理或统计时,独立 awk 脚本的优势更加明显。我们将以统计为例,展示从简单到复杂的演进。

示例 5:统计指定列的脚本

创建 stats.awk

!/usr/bin/awk -f
BEGIN {
    if (col == "") {
        print "Error: Please specify column using -v col=NUM"
        exit 1
    }
    print "Calculating stats for column", col
    sum = 0
    count = 0
}
{
    sum += $col
    count++
}
END {
    if (count == 0) {
        print "No data processed."
    } else {
        print "Sum:", sum
        print "Average:", sum / count
    }
}

运行:

chmod +x stats.awk
./stats.awk -v col=2 data.txt

输出:

Calculating stats for column 2
Sum: 250
Average: 83.3333

解析:

  • -v col=2:指定统计第 2 列。
  • $col:动态引用字段,例如 $2,这里是 awk 的字段引用,而非 Bash 参数。
  • 健壮性:检查输入数据是否为空。
示例 6:多功能统计脚本

扩展功能,添加最大值和最小值统计,创建 fullstats.awk

!/usr/bin/awk -f
BEGIN {
    if (col == "") {
        print "Error: Please specify column using -v col=NUM"
        exit 1
    }
    print "Analyzing column", col
    sum = 0
    count = 0
    min = ""
    max = ""
}
{
    value = $col
    sum += value
    count++
    if (min == "" || value < min) min = value
    if (max == "" || value > max) max = value
}
END {
    if (count == 0) {
        print "No data processed."
    } else {
        print "Count:", count
        print "Sum:", sum
        print "Average:", sum / count
        print "Min:", min
        print "Max:", max
    }
}

运行:

chmod +x fullstats.awk
./fullstats.awk -v col=2 data.txt

输出:

Analyzing column 2
Count: 3
Sum: 250
Average: 83.3333
Min: 75
Max: 90

解析:

  • $col:动态字段引用,处理指定列。
  • value = $col:将字段值存入变量,便于逻辑操作。
  • 模块化:功能丰富,可扩展为更多统计需求。

Bash 与 AWK 的结合:灵活调用

有时,我们需要在 Bash 脚本中调用 awk 脚本,进一步增强灵活性。例如:

创建 run_stats.sh

!/bin/bash
if [ $# -lt 2 ]; then
    echo "Usage: $0 <column> <file>"
    exit 1
fi
column=$1
file=$2
./fullstats.awk -v col="$column" "$file"

运行:

chmod +x run_stats.sh
./run_stats.sh 2 data.txt

输出与 fullstats.awk 相同。这种方式将 Bash 的参数处理与 awk 的逻辑分离,适合复杂工作流。


脚本化的优势与注意事项

脚本化将 awk 从临时命令提升为可复用工具,适用于:

  • 复杂逻辑:多步骤处理或统计。
  • 重复任务:避免重复输入长命令。
  • 协作开发:脚本文件易于共享。

注意事项

  1. 参数区分:Bash 的 $1 是命令行参数,awk$1 是字段,传递时需用 -v 桥接。
  2. 字段有效性$col 使用时确保 col 是整数,否则会导致错误。
  3. 健壮性:添加参数检查和错误提示。

AWK 高级特性:提升处理精度

awk 不仅是文本处理的瑞士军刀,还提供了一系列高级技巧,能显著提升文本处理的灵活性和精度,让你在复杂场景中也能游刃有余。

1. $1 = $1:强制字段重建,去除多余空白

在处理字段时,输入数据中可能夹杂着多余的空格或制表符,这会影响后续的字段处理。$1 = $1 这一看似简单的操作,却能强制 awk 重新解析字段,去除多余空白,确保字段的纯净。

示例:messy.txt

apple  10  extra
banana 20  spaces

命令:

awk '{
    
    $1 = $1; print $0}' messy.txt

输出:

apple 10 extra
banana 20 spaces

原理: 赋值操作会触发 awk 重新构建当前记录的字段,按照 FS(Field Separator,字段分隔符)重新分割,从而去除多余的空白。

2. OFS:规范化输出,控制字段分隔符

OFS(Output Field Separator,输出字段分隔符)变量允许我们自定义输出字段之间的分隔符,从而规范化输出格式。

示例:使用逗号分隔字段

awk 'BEGIN {OFS=","} {
    
    $1 = $1; print $0}' messy.txt

输出:

apple,10,extra
banana,20,spaces

3. 自定义函数:代码复用,提高效率

awk 允许我们定义自己的函数,将常用的代码块封装起来,提高代码的复用性和可维护性。

示例:计算平方

data.txt

1
2
3
4
awk 'function square(x) {return x*x} {print square($1)}' data.txt

输出:

1
4
9
16

4. 多维数组模拟:复杂数据结构的处理

awk 本身不支持多维数组,但我们可以通过字符串拼接的方式模拟多维数组,用于表示复杂的数据结构。

示例:模拟矩阵

data.txt

1 1 10
1 2 20
2 1 30
2 2 40
awk '{matrix[$1","$2] = $3} END {for (k in matrix) print k, matrix[k]}' data.txt

输出:

1,1 10
1,2 20
2,1 30
2,2 40

5. 动态正则匹配:灵活的模式匹配

awk 允许我们使用变量作为正则表达式,实现动态的模式匹配,这在处理需要根据不同条件进行匹配的场景中非常有用。

示例:根据变量匹配行

data.txt

apple pie
banana split
apple juice
orange juice
awk -v pat="apple" '$0 ~ pat {print $0}' data.txt

输出:

apple pie
apple juice

6. getline:精确控制输入流

getline 函数允许我们从文件或管道中读取输入,并将其存储在变量中,这在需要精确控制输入流的场景中非常有用。

示例:读取文件下一行

file1.txt

line1
line2

file2.txt

next1
next2
awk '{getline next_line < "file2.txt"; print $0, next_line}' file1.txt

输出:

line1 next1
line2 next2

7. system():执行外部命令

system() 函数允许我们在 awk 脚本中执行外部命令,并将命令的输出作为字符串返回,这在需要与外部系统交互的场景中非常有用。

示例:执行 date 命令

data.txt

dummy
awk '{print "Current date:", system("date")}' data.txt

输出:

Current date: Fri May 24 10:00:00 CST 2024

8. strftime():格式化日期和时间

strftime() 函数允许我们根据指定的格式字符串格式化日期和时间,这在处理包含日期和时间的数据时非常有用。

示例:格式化日期

timestamps.txt

1678886400 # 2023-03-15 00:00:00
1678972800 # 2023-03-16 00:00:00
awk '{print strftime("%Y-%m-%d", $1)}' timestamps.txt

输出:

2023-03-15
2023-03-16

小结

本篇下篇深入探讨了 awk 的高级特性和脚本化应用,涵盖以下要点:

  • 关联数组:高效统计和分组,适合日志分析与数据汇总。
  • 多文件处理:通过 FNRNR 实现文件合并与差异分析。
  • 脚本编写:结合 -v 参数实现模块化、可复用处理。
  • 高级特性:如 $1 = $1 去除空白,提升处理精度。

通过这些内容,你已掌握 awk 从基础到高级的全貌,能够应对从简单字段提取到复杂数据分析的各种任务。awk 的学习之旅虽告一段落,但其应用潜力无穷,期待你在实践中不断挖掘其价值!