Bash 零基础教程

awk 文本处理神器

awk 是一款在模式扫描和数据处理方面表现卓越的强大文本处理工具。它逐行读取输入文件,根据分隔符将每一行分割成多个字段,然后对这些字段执行操作。这使得它在提取特定数据、重新格式化输出以及从结构化文本文件生成报告方面特别有效。

1. awk 基础:结构与基本用法

awk 命令的基本结构是 awk '模式 { 动作 }' 输入文件

awk 处理 输入文件(如果没有指定文件,则处理标准输入)的每一行。对于每一行,它会检查是否匹配指定的 模式 (pattern)。如果该行匹配,awk 就会执行与该模式关联的 动作 (action)。

  • 如果没有提供模式,动作将对每一行执行。
  • 如果没有提供动作,awk 将打印整行。

1.1 字段与字段分隔符

awk 会自动将每行输入划分为多个字段 (fields)。默认情况下,空白字符(空格或制表符)充当字段分隔符。

  • 第一个字段被引用为 $1,第二个为 $2,依此类推。
  • 整行数据用 $0 表示。
  • 当前记录(行)中的字段数量存储在内置变量 NF (Number of Fields) 中。
  • 到目前为止处理的记录总数(通常就是行号)存储在 NR (Number of Records) 中。

考虑一个简单的 data.txt 文件:

Name Age City
Alice 30 NewYork
Bob 24 London
Charlie 35 Paris

只打印 data.txt 中的名字(第一个字段):

awk '{ print $1 }' data.txt

此命令处理每一行并打印第一个字段 ($1) 的内容。

打印名字和城市(第一和第三个字段):

awk '{ print $1, $3 }' data.txt

print 语句中,$1$3 之间的逗号,默认会让 awk 在输出时在字段之间加一个空格。

打印整行以及字段数量:

awk '{ print "Line:", $0, "Fields:", NF }' data.txt

这演示了如何访问 $0(整行)和 NF(字段数)。

自定义字段分隔符 (-F)

有时,数据由空白字符以外的字符分隔,例如 CSV 文件中的逗号或 /etc/passwd 中的冒号。-F 选项(或内部的 FS 变量)可以指定备用字段分隔符。

示例 grades.csv

Student,Math,Science,English
Alice,90,85,92
Bob,78,88,80
Charlie,95,90,88

grades.csv 打印学生姓名及其数学成绩:

awk -F',' '{ print $1, $2 }' grades.csv

-F',' 将字段分隔符设置为逗号。

1.2 模式 (Patterns):筛选行

awk 中的模式可以是正则表达式、关系表达式或两者的组合。如果提供了模式,则仅对匹配该模式的行执行动作。

正则表达式模式: 这些工作方式与 grep 类似,但在 awk 上下文中。包含在正斜杠内的模式 /pattern/ 将匹配包含该正则表达式的行。

data.txt 中打印城市为 "NewYork" 的行:

awk '/NewYork/ { print $0 }' data.txt

如果在该行任意位置找到 "NewYork",它就会打印整行。

打印名字以 'A' 开头的人的名字:

awk '/^A/ { print $1 }' data.txt

这里使用正则表达式 ^A 来匹配第一个字符是 'A' 的行。

关系表达式模式: 这些模式使用关系运算符(==, !=, <, <=, >, >=)来比较字段值或变量。

grades.csv 中打印数学成绩大于 80 的学生姓名和成绩:

awk -F',' '$2 > 80 { print $1, $2 }' grades.csv

这里,$2 > 80 就是模式。它检查第二个字段(数学成绩)在数值上是否大于 80。

打印数学成绩正好是 90 的学生姓名:

awk -F',' '$2 == 90 { print $1, $2 }' grades.csv

== 运算符执行精确比较。

1.3 动作 (Actions):要做什么

动作包含在大括号 {} 中,由一个或多个 awk 语句组成。常见的动作包括:

  • print 语句: 如前所述,打印字段或整行。
  • 变量awk 支持用户自定义变量。
  • 算术运算: 执行数学计算。
  • 条件语句 (if-else): 控制动作内部的代码流程。
  • 循环 (for, while): 在动作内部进行迭代。

计算 grades.csv 中的数学平均分:

awk -F',' '
NR==1 { next } # 跳过标题行
{
    total_math_score += $2 # 累加分数
    count++                # 统计学生人数
}
END {
    print "总人数:", count
    print "数学平均分:", total_math_score / count
}' grades.csv

这个例子引入了几个新概念:

  • NR==1 { next }: 匹配第一行(NR 是记录号)的模式。next 告诉 awk 直接跳到下一行输入,不执行后面的代码。
  • total_math_score += $2: 将当前学生的数学分数加到一个总和变量中。awk 变量是动态类型的,初始值默认为 0 或空字符串。
  • count++: 增加学生计数器。
  • END { ... }: 一个特殊的模式,它的动作在所有输入行都处理完毕后执行。这非常适合用于汇总数据。

2. awk 高阶特性

awk 提供了更高级的特性来进行复杂的数据操作。

2.1 BEGIN 和 END 块

BEGINEND 是特殊的模式,允许在读取任何输入行之前 (BEGIN) 或在处理完所有输入行之后 (END) 执行特定的动作。

  • BEGIN { 动作 }: 在 awk 开始处理第一行输入之前执行一次。常用于初始化变量、打印表头或设置字段分隔符 (FS)。
  • END { 动作 }: 在 awk 处理完所有输入行之后执行一次。常用于打印汇总统计信息、总计或表尾。

使用 BEGINENDgrades.csv 打印格式化报表的示例:

awk -F',' '
BEGIN {
    print "--- 学生成绩报告 ---"
    print "姓名\t数学\t科学\t英语"
    sum_math = 0
    sum_science = 0
    sum_english = 0
    count_students = 0
}
NR==1 { next } # 跳过表头
{
    print $1,"\t",$2,"\t",$3,"\t",$4
    sum_math += $2
    sum_science += $3
    sum_english += $4
    count_students++
}
END {
    print "-----------------------------"
    print "成绩汇总:"
    print "数学平均分:", sum_math / count_students
    print "科学平均分:", sum_science / count_students
    print "英语平均分:", sum_english / count_students
    print "-----------------------------"
}' grades.csv

BEGIN 块中,我们设置了报表表头并初始化了求和变量。主块处理数据并累加总和。最后 END 块计算并打印平均值。

2.2 awk 核心内置变量

除了 NF(字段数量)、NR(记录/行号)和 $0(整行)之外,awk 还有几个非常有用的内置变量:

  • FS (Field Separator): 输入字段分隔符。可以在 BEGIN 块中设置,也可以通过 -F 命令行选项设置。
  • OFS (Output Field Separator): 打印时使用的输出字段分隔符。默认是一个空格。
  • RS (Record Separator): 输入记录分隔符(默认为换行符)。
  • ORS (Output Record Separator): 输出记录分隔符(默认为换行符)。
  • FILENAME: 当前正在处理的输入文件的名称。

将输出字段分隔符更改为逗号并打印 data.txt

awk 'BEGIN { OFS="," } { print $1, $2, $3 }' data.txt

这将打印由逗号而不是空格分隔的字段。

打印每条记录的文件名:

awk '{ print FILENAME, NR, $0 }' data.txt

2.3 条件逻辑与循环

awk 在动作块内支持 if-else 语句和 for/while 循环,从而实现更复杂的逻辑。

if-else 语句:

awk -F',' '
NR==1 { next }
{
    if ($2 > 90) {
        status = "优秀"
    } else if ($2 > 80) {
        status = "良好"
    } else {
        status = "一般"
    }
    print $1, $2, status
}' grades.csv

此脚本根据数学分数添加了一个 "状态" 评估列。

for 循环:
打印 data.txt 每行中的所有字段,并在前面加上它们的字段号:

awk '{
    for (i=1; i<=NF; i++) {
        print "字段", i, ":", $i
    }
    print "---"
}' data.txt

循环从 1 迭代到 NF(字段总数),逐个打印每个字段。

2.4 用户自定义函数

为了使代码更具组织性和可重用性,awk 允许定义函数。

awk '
function calculate_average(score1, score2, score3) {
    return (score1 + score2 + score3) / 3
}
NR==1 { next }
{
    avg = calculate_average($2, $3, $4)
    print $1, avg
}' grades.csv

这个例子定义了一个 calculate_average 函数来计算三个分数的平均值,然后用它来打印每个学生的姓名及其整体平均成绩。

3. 实际应用案例与演示

3.1 提取和汇总日志数据(案例研究)

考虑一个名为 access.log 的 Web 服务器日志文件(类似于我们解析日志文件以识别问题的实战案例)。

192.168.1.1 - [10/Nov/2023:10:00:01 +0000] "GET /index.html HTTP/1.1" 200 1234
192.168.1.2 - [10/Nov/2023:10:00:05 +0000] "GET /about.html HTTP/1.1" 200 567
192.168.1.1 - [10/Nov/2023:10:00:10 +0000] "POST /login HTTP/1.1" 401 200
192.168.1.3 - [10/Nov/2023:10:00:15 +0000] "GET /images/logo.png HTTP/1.1" 200 890
192.168.1.2 - [10/Nov/2023:10:00:20 +0000] "GET /nonexistent HTTP/1.1" 404 150
192.168.1.1 - [10/Nov/2023:10:00:25 +0000] "GET /index.html HTTP/1.1" 200 1234

每一行包含:IP 地址、时间戳、请求方法/路径/协议、状态码和发送的字节数。字段主要由空格分隔。

任务 1:提取所有状态码为 404 的请求及其对应的 IP 地址。
如果我们仔细计算(将空格视为分隔符),状态码是第 9 个字段。IP 地址是第 1 个字段。

awk '$9 == 404 { print "IP:", $1, "请求路径:", $7, "状态:", $9 }' access.log
  • $9 == 404 匹配第 9 个字段是 404 的行。
  • print ... 打印 IP、请求路径(第 7 个字段,在引号后面)和状态码。

任务 2:统计总请求数以及按状态码分类的请求数。

awk '
{
    total_requests++
    status_counts[$9]++ # 使用关联数组
}
END {
    print "总请求数:", total_requests
    print "--- 状态码统计 ---"
    for (status_code in status_counts) {
        print "状态", status_code, ":", status_counts[status_code]
    }
}' access.log
  • total_requests++ 对每一行递增计数器。
  • status_counts[$9]++ 使用了一个名为 status_counts 的关联数组(或称为哈希表/字典)。状态码 ($9) 作为键 (Key),其对应的值在每次出现时递增。
  • END 块遍历 status_counts 数组以打印最终摘要。

3.2 数据转换:重新格式化数据

假设我们要对前面的 data.txt 进行重新排序:

Name Age City
Alice 30 NewYork
Bob 24 London
Charlie 35 Paris

任务:将 data.txt 重新格式化为 "City: <City>, Name: <Name>, Age: <Age>"

awk '
NR==1 { print "City: City, Name: Name, Age: Age"; next } # 处理表头
{
    print "City:", $3 ", Name:", $1 ", Age:", $2
}' data.txt

此示例打印一个自定义表头,然后对后续每一行的字段重新排序并添加前缀。