AWK & SED 生产力教程 / 第 4 章:AWK 基础
第 4 章:AWK 基础
AWK 不仅仅是一个命令,它是一门完整的编程语言——只不过恰好擅长文本处理。
4.1 AWK 的工作模型
核心概念
输入文件
│
▼
┌─────────────────────────────────────┐
│ for each line in input: │
│ 1. 读取一行 │
│ 2. 按 FS 分割成字段 $1, $2, ... │
│ 3. 检查是否匹配 模式 (pattern) │
│ 4. 如果匹配 → 执行 动作 (action) │
│ 5. 如果没有动作 → 打印整行 │
│ 6. 如果没有模式 → 对所有行执行 │
└─────────────────────────────────────┘
│
▼
输出 (stdout)
基本语法
awk 'pattern { action }' file
awk -F分隔符 'pattern { action }' file
awk -f script.awk file
AWK 程序结构
awk '
BEGIN { 初始化代码 } # 在处理第一行之前执行一次
pattern1 { 动作1 } # 对匹配的每一行执行
pattern2 { 动作2 } # 可以有多个模式-动作对
END { 结束代码 } # 在处理完最后一行之后执行一次
' file
执行顺序:
BEGIN → 只执行一次(在任何输入之前)
逐行处理 → 对每一行依次检查所有 pattern
END → 只执行一次(在所有输入之后)
你的第一个 AWK 程序
$ echo -e "Alice 90\nBob 85\nCarol 92" | awk '
BEGIN { print "=== 成绩单 ===" }
{ print $1, "的成绩:", $2 }
END { print "=== 结束 ===" }
'
→ === 成绩单 ===
→ Alice 的成绩: 90
→ Bob 的成绩: 85
→ Carol 的成绩: 92
→ === 结束 ===
4.2 字段与记录
字段引用
| 变量 | 说明 | 示例 |
|---|---|---|
$0 | 整行内容 | awk '{print $0}' |
$1 | 第 1 个字段 | awk '{print $1}' |
$2 | 第 2 个字段 | awk '{print $2}' |
$N | 第 N 个字段 | awk '{print $N}' |
$NF | 最后一个字段 | awk '{print $NF}' |
$(NF-1) | 倒数第二个字段 | awk '{print $(NF-1)}' |
$ echo "Alice 90 85 92" | awk '{print "第一个字段:", $1}'
→ 第一个字段: Alice
$ echo "Alice 90 85 92" | awk '{print "最后一个字段:", $NF}'
→ 最后一个字段: 92
$ echo "Alice 90 85 92" | awk '{print "字段数量:", NF}'
→ 字段数量: 4
📌 注意:修改字段值后
$0会自动重建:$ echo "a b c" | awk '{$2="X"; print $0}' → a X c
字段分隔符
# 方法一:-F 选项
$ echo "Alice:90:85" | awk -F: '{print $1, $2}'
→ Alice 90
# 方法二:在 BEGIN 中设置 FS
$ echo "Alice,90,85" | awk 'BEGIN{FS=","} {print $1, $2}'
→ Alice 90
# 方法三:使用正则作为分隔符
$ echo "Alice 90 85" | awk -F'[[:space:]]+' '{print $1, $2}'
→ Alice 90
# 多字符分隔符
$ echo "Alice::90::85" | awk -F'::' '{print $1, $2}'
→ Alice 90
输出分隔符(OFS)
# 默认输出分隔符是空格
$ echo "a b c" | awk '{print $1, $2, $3}'
→ a b c
# 设置输出分隔符
$ echo "a b c" | awk 'BEGIN{OFS=","} {print $1, $2, $3}'
→ a,b,c
# 修改字段后 OFS 生效
$ echo "a b c" | awk 'BEGIN{OFS=","} {$1=$1; print}'
→ a,b,c
# ⚠️ 注意:需要 $1=$1 来触发 $0 重建
💡 技巧:
$1=$1是一个常见的"伪操作",目的是让 AWK 用新的 OFS 重建$0。
记录分隔符
# 默认记录分隔符是换行符 \n
# 设置记录分隔符为逗号
$ echo "a,b,c,d" | awk 'BEGIN{RS=","} {print NR, $0}'
→ 1 a
→ 2 b
→ 3 c
→ 4 d
# 处理多行记录(用空行分隔)
cat > records.txt << 'EOF'
Name: Alice
Age: 30
Name: Bob
Age: 25
EOF
$ awk 'BEGIN{RS=""; FS="\n"} {
for(i=1; i<=NF; i++) {
if($i ~ /^Name:/) { split($i, a, ": "); name=a[2] }
if($i ~ /^Age:/) { split($i, a, ": "); age=a[2] }
}
print name, age
}' records.txt
→ Alice 30
→ Bob 25
4.3 内置变量
完整内置变量表
| 变量 | 说明 | 默认值 |
|---|---|---|
$0 | 当前记录(整行) | — |
$1..$N | 字段 | — |
NR | 已读取的总记录数 | — |
NF | 当前记录的字段数 | — |
FNR | 当前文件中的记录数 | — |
FS | 输入字段分隔符 | " "(空格) |
OFS | 输出字段分隔符 | " "(空格) |
RS | 输入记录分隔符 | "\n"(换行) |
ORS | 输出记录分隔符 | "\n"(换行) |
FILENAME | 当前文件名 | — |
ARGC | 命令行参数个数 | — |
ARGV | 命令行参数数组 | — |
ENVIRON | 环境变量数组 | — |
OFMT | 数字输出格式 | "%.6g" |
CONVFMT | 数字到字符串转换格式 | "%.6g" |
SUBSEP | 数组下标分隔符 | "\034" |
RSTART | match() 匹配的起始位置 | — |
RLENGTH | match() 匹配的长度 | — |
NR vs FNR
# NR 是全局行号,FNR 是文件内行号
$ awk '{print FILENAME, NR, FNR, $0}' file1.txt file2.txt
# 合并两个文件时很有用
$ awk 'FNR==1 && NR!=1 {print "---"} {print}' file1.txt file2.txt
# 实用示例:打印行号
$ awk '{print NR": "$0}' file.txt
# 只打印有内容的行的行号
$ awk 'NF>0 {print NR": "$0}' file.txt
多文件处理
# AWK 可以同时处理多个文件
$ awk '{print FILENAME": "$0}' file1.txt file2.txt file3.txt
# 每个文件分别计数
$ awk '{count[FILENAME]++} END{for(f in count) print f, count[f]}' *.txt
4.4 模式(Pattern)
模式类型
| 模式类型 | 说明 | 示例 |
|---|---|---|
/正则/ | 正则表达式匹配 | /error/ {print} |
关系表达式 | 比较运算 | $3 > 100 |
组合模式 | && || ! | $1=="GET" && $9>=400 |
范围模式 | 起始模式,结束模式 | /BEGIN/,/END/ |
BEGIN | 处理输入之前 | BEGIN {print "start"} |
END | 处理输入之后 | END {print "done"} |
空模式 | 匹配所有行 | {print} |
正则表达式模式
# 包含 error 的行
$ awk '/error/ {print}' logfile
# 不包含 error 的行
$ awk '!/error/ {print}' logfile
# 匹配特定字段
$ awk '$1 ~ /^192\.168/ {print}' access.log
# 字段不匹配
$ awk '$1 !~ /^192\.168/ {print}' access.log
# 精确匹配(注意 == 和 ~ 的区别)
$ awk '$1 == "192.168.1.1" {print}' access.log
关系表达式模式
# 数值比较
$ awk '$3 > 100' data.txt # 第 3 列大于 100
$ awk '$3 >= 90 && $3 <= 100' data.txt # 范围
$ awk 'NF > 5' data.txt # 字段数大于 5
$ awk 'NR == 10' data.txt # 第 10 行
# 字符串比较
$ awk '$1 == "admin"' users.txt # 精确匹配
$ awk '$1 != "admin"' users.txt # 不等于
# 混合比较
$ awk '$1 == "GET" && $9 >= 400' access.log
范围模式
# 从匹配第一个模式的行到匹配第二个模式的行(含边界)
$ awk '/BEGIN/,/END/' file
# 从第 5 行到第 10 行
$ awk 'NR==5, NR==10' file
# 范围模式可以和命令结合
$ awk '/error/,/resolved/ {print NR": "$0}' logfile
⚠️ 注意:范围模式可能产生意想不到的结果。如果"结束"模式不匹配,范围会持续到文件末尾。
BEGIN 和 END
# 计算平均值
$ awk 'BEGIN{sum=0; count=0}
{sum+=$1; count++}
END{print "平均值:", sum/count}' data.txt
# 打印表头和表尾
$ awk '
BEGIN { print "========== 报表 ==========" }
{ print NR". "$0 }
END { print "========== 共 "NR" 行 ==========" }
' data.txt
4.5 AWK 中的正则表达式
基本用法
# 匹配(ERE 语法)
$ awk '/pattern/' file # $0 包含 pattern
$ awk '!/pattern/' file # $0 不包含 pattern
$ awk '$2 ~ /pattern/' file # $2 包含 pattern
$ awk '$2 !~ /pattern/' file # $2 不包含 pattern
常用正则示例
# 匹配 IP 地址(简化)
$ awk '/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ {print $1}' access.log
# 匹配邮箱
$ awk '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/' file
# 匹配日期格式 YYYY-MM-DD
$ awk '/[0-9]{4}-[0-9]{2}-[0-9]{2}/' logfile
# 匹配 HTTP 状态码 4xx 或 5xx
$ awk '$9 ~ /^[45][0-9]{2}$/' access.log
正则中的特殊字符
| 字符 | 含义 | 示例 |
|---|---|---|
. | 任意单个字符 | a.c 匹配 abc, aXc |
* | 前一项零次或多次 | ab*c 匹配 ac, abc, abbc |
+ | 前一次或多次(ERE) | ab+c 匹配 abc, abbc |
? | 前一项零次或一次(ERE) | ab?c 匹配 ac, abc |
^ | 行首 | ^error |
$ | 行尾 | end$ |
[] | 字符类 | [abc] 匹配 a, b, c |
[^] | 否定字符类 | [^abc] 匹配非 a, b, c |
| | 或(ERE) | cat|dog |
() | 分组(ERE) | (ab)+ |
{n,m} | 重复次数(ERE) | a{2,4} |
\b | 单词边界 | \berror\b |
📌 重要:AWK 默认使用 ERE(扩展正则表达式),不像 SED 默认使用 BRE。
4.6 输出格式化
print vs printf
# print — 简单输出
$ awk '{print $1, ":", $2}' file
# printf — 格式化输出(类似 C 语言)
$ awk '{printf "%-20s : %10d\n", $1, $2}' file
printf 格式说明符
| 说明符 | 含义 | 示例 |
|---|---|---|
%s | 字符串 | printf "%s", $1 |
%d | 整数 | printf "%d", $2 |
%f | 浮点数 | printf "%.2f", $3 |
%e | 科学记数法 | printf "%e", $3 |
%x | 十六进制 | printf "%x", $2 |
%o | 八进制 | printf "%o", $2 |
%% | 百分号字面量 | printf "100%%" |
格式修饰符
# 宽度
$ awk '{printf "%20s\n", $1}' file # 右对齐,20字符宽
$ awk '{printf "%-20s\n", $1}' file # 左对齐,20字符宽
# 精度
$ awk '{printf "%.2f\n", $3}' file # 保留2位小数
# 前导零
$ awk '{printf "%05d\n", $2}' file # 5位数字,不足补零
# 组合
$ awk '{printf "%-15s %10.2f %8d\n", $1, $2, $3}' data.txt
🏢 业务场景:格式化报表
cat > sales.txt << 'EOF'
ProductA 1500 29.99
ProductB 800 49.99
ProductC 2200 15.50
ProductD 350 99.99
EOF
$ awk '
BEGIN {
printf "%-15s %10s %10s %12s\n", "产品", "销量", "单价", "总金额"
printf "%-15s %10s %10s %12s\n", "----", "----", "----", "------"
}
{
total = $2 * $3
printf "%-15s %10d %10.2f %12.2f\n", $1, $2, $3, total
sum += total
}
END {
printf "%-15s %10s %10s %12s\n", "----", "----", "----", "------"
printf "%-15s %10s %10s %12.2f\n", "合计", "", "", sum
}
' sales.txt
→ 产品 销量 单价 总金额
→ ---- ---- ---- ------
→ ProductA 1500 29.99 44985.00
→ ProductB 800 49.99 39992.00
→ ProductC 2200 15.50 34100.00
→ ProductD 350 99.99 34996.50
→ ---- ---- ---- ------
→ 合计 154073.50
4.7 算术与字符串运算
算术运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
+ - * / | 加减乘除 | $1 + $2 |
% | 取模 | $1 % 2 |
^ 或 ** | 幂 | $1 ^ 2 |
++ -- | 自增自减 | count++ |
+= -= *= /= | 复合赋值 | sum += $1 |
字符串运算
# 字符串连接(直接放在一起)
$ echo "hello" | awk '{print $1 " world"}'
→ hello world
# 字符串长度
$ echo "hello" | awk '{print length($0)}'
→ 5
# 子串提取
$ echo "abcdef" | awk '{print substr($0, 2, 3)}'
→ bcd
# 字符串查找
$ echo "hello world" | awk '{print index($0, "world")}'
→ 7
# 字符串替换
$ echo "hello world" | awk '{print sub(/world/, "AWK", $0); print}'
→ 1
→ hello AWK
# 全局替换
$ echo "aaa bbb aaa" | awk '{print gsub(/aaa/, "xxx", $0); print}'
→ 2
→ xxx bbb xxx
# 分割字符串
$ echo "a:b:c" | awk '{n=split($0, arr, ":"); for(i=1;i<=n;i++) print arr[i]}'
→ a
→ b
→ c
类型转换
AWK 是弱类型语言,字符串和数字会自动转换:
# 字符串转数字
$ echo "42" | awk '{print $1 + 8}'
→ 50
# 数字转字符串
$ echo "42" | awk '{print "value is " $1}'
→ value is 42
# 比较时的注意事项
$ echo "9" | awk '{print ($1 < 10) ? "yes" : "no"}'
→ yes
$ echo "9" | awk '{print ($1 < "10") ? "yes" : "no"}'
→ no # 字符串比较: "9" > "1" 所以 "9" > "10"
⚠️ 陷阱:字符串比较和数值比较不一样!
9 < 10是 true,但"9" < "10"是 false(因为按字典序比较)。
4.8 流程控制
条件语句
# 三元运算符
$ awk '{print ($2 > 60 ? "PASS" : "FAIL")}' scores.txt
# if-else
$ awk '{
if ($2 > 90) grade = "A"
else if ($2 > 80) grade = "B"
else if ($2 > 70) grade = "C"
else grade = "F"
print $1, grade
}' scores.txt
循环语句
# for 循环
$ awk '{for(i=1; i<=NF; i++) print $i}' file
# while 循环
$ awk '{
i = 1
while (i <= NF) {
print $i
i++
}
}' file
# do-while 循环
$ awk 'BEGIN {
i = 1
do {
print i
i++
} while (i <= 5)
}'
控制语句
# next — 跳过当前行,读取下一行
$ awk '/^#/ {next} {print}' config.txt # 跳过注释行
# nextfile — 跳过当前文件(GNU AWK)
$ awk 'FNR==1 && /error/ {nextfile} {print}' *.log
# exit — 退出 AWK
$ awk '/^END_MARKER/ {exit} {print}' file
# break 和 continue
$ awk '{for(i=1; i<=NF; i++) {if($i=="skip") continue; print $i}}' file
4.9 实战案例
🏢 场景一:分析 Nginx 访问日志
# 日志格式示例
# 192.168.1.1 - - [15/Jan/2024:10:23:45 +0800] "GET /index.html HTTP/1.1" 200 1234
# 提取 IP、请求路径和状态码
$ awk '{
ip = $1
path = $7
status = $9
print ip, path, status
}' access.log
# 统计各状态码数量
$ awk '{count[$9]++} END{for(s in count) print s, count[s]}' access.log | sort
# 找出请求最多的 IP
$ awk '{count[$1]++} END{for(ip in count) print count[ip], ip}' access.log | sort -rn | head -10
# 统计每小时的请求数
$ awk -F'[[ :]' '{hour=$6; count[hour]++} END{for(h in count) print h, count[h]}' access.log | sort
🏢 场景二:系统资源监控
# 解析 free 命令输出
$ free -m | awk '/^Mem:/ {
total = $2
used = $3
free = $4
printf "内存使用率: %.1f%%\n", used/total*100
}'
# 解析 df 命令输出
$ df -h | awk 'NR>1 {
if ($5+0 > 80) print "警告: "$6" 使用率 "$5
}'
# 解析 top 输出(CPU)
$ top -bn1 | awk 'NR==3 {
printf "CPU 使用率: %.1f%%\n", 100-$8
}'
🏢 场景三:处理 /etc/passwd
# 列出所有用户及其 shell
$ awk -F: '{printf "%-15s %s\n", $1, $7}' /etc/passwd
# 统计各 shell 的使用数量
$ awk -F: '{shell[$7]++} END{for(s in shell) print shell[s], s}' /etc/passwd
# 列出 UID >= 1000 的普通用户
$ awk -F: '$3 >= 1000 {print $1, $3, $6}' /etc/passwd
# 列出没有设置密码的用户(需要 root 权限读 /etc/shadow)
$ awk -F: '($2 == "" || $2 == "!") {print $1}' /etc/shadow 2>/dev/null
🏢 场景四:处理 CSV 文件
cat > employees.csv << 'EOF'
Name,Department,Salary
Alice,Engineering,15000
Bob,Marketing,12000
Carol,Engineering,16000
Dave,Marketing,11000
Eve,Engineering,14000
EOF
# 计算各部门平均工资
$ awk -F, 'NR>1 {
dept_sum[$2] += $3
dept_count[$2]++
}
END {
for(d in dept_sum)
printf "%-15s 平均工资: %.0f\n", d, dept_sum[d]/dept_count[d]
}' employees.csv
4.10 命令速查
# 基本用法
awk '{print}' file # 打印所有行
awk '{print $1}' file # 打印第 1 列
awk -F: '{print $1}' file # 指定分隔符
awk 'NR==5' file # 第 5 行
awk 'NR>=3 && NR<=7' file # 第 3-7 行
awk '/pattern/' file # 匹配行
awk '{print NR, $0}' file # 带行号
# 统计
awk '{sum+=$1} END{print sum}' file # 求和
awk '{sum+=$1; n++} END{print sum/n}' file # 平均值
awk 'END{print NR}' file # 行数
awk '{print NF}' file # 每行字段数
# 格式化
awk '{printf "%s %d\n", $1, $2}' file
awk '{printf "%-20s %10.2f\n", $1, $2}' file
扩展阅读
- GNU AWK Manual
- AWK by Example
- 《The AWK Programming Language》— Aho, Weinberger, Kernighan
下一章:第 5 章:AWK 进阶 — 掌握数组、用户函数和高级编程技巧。