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 块
BEGIN 和 END 是特殊的模式,允许在读取任何输入行之前 (BEGIN) 或在处理完所有输入行之后 (END) 执行特定的动作。
BEGIN { 动作 }: 在awk开始处理第一行输入之前执行一次。常用于初始化变量、打印表头或设置字段分隔符 (FS)。END { 动作 }: 在awk处理完所有输入行之后执行一次。常用于打印汇总统计信息、总计或表尾。
使用 BEGIN 和 END 为 grades.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.txt2.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.logtotal_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此示例打印一个自定义表头,然后对后续每一行的字段重新排序并添加前缀。