强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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"
RSTARTmatch() 匹配的起始位置
RLENGTHmatch() 匹配的长度

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 — 简单输出
$ 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

扩展阅读


下一章:第 5 章:AWK 进阶 — 掌握数组、用户函数和高级编程技巧。