shell05-awk 文本处理
awk 文本处理
目录
- awk 的定位:三剑客中负责「处理列」
- awk 的工作模型:记录与字段
- awk 基本语法与字段变量
- BEGIN 与 END:处理前/后的钩子
- awk 的模式匹配与条件
- awk 的内建变量
- awk 中的变量、运算与格式化
- awk 统计实战
- awk 与 grep、sed 的协作
- awk 脚本文件与进阶
- 总结与练习
- 参考资料
0. awk 的定位:三剑客中负责「处理列」
前两剑——grep 筛行、sed 改内容——都是在「行」的层面操作。但很多真实需求是按列来的:从日志里取 IP、从 passwd 里取用户名、统计每个状态码的出现次数。这就是第三剑 awk 的用武之地。
文本三剑客分工:
- grep:筛行;
- sed:改内容(行级);
- awk:按列处理、统计、格式化。
awk 最擅长:
- 按列提取(如取 /etc/passwd 的第 1 列用户名);
- 统计汇总(求和、计数、求平均值);
- 条件过滤(只处理满足条件的行);
- 格式化输出。
awk 本质是一门小而完整的编程语言,可以用它写循环、算术、拼接,甚至写完整的文本处理程序。也正因为如此,它是三剑客里最强也最值得学的一个。
一、awk 的工作模型:记录与字段
1.1 记录与字段
要理解 awk,先理解它的核心工作模型:awk 逐行处理文本,每一行它都拆成两个层次——记录和字段。
- 记录(record):默认是「一行」。awk 每次处理一条记录;
- 字段(field):一条记录(一行)里,按分隔符切成的「几列」。
看 /etc/passwd 的一行(用冒号分隔):
一行: root x 0 0 root /root /bin/bash
字段: $1 $2 $3 $4 $5 $6 $7
awk 用 $1、$2... 引用各字段,$0 代表整行。所以 $1 是第 1 列、$7 是第 7 列。这和第 1 篇脚本里的「位置参数 $1」是两回事——awk 里 $ 后面跟的是字段编号。
1.2 默认分隔符:空白
awk 默认用「一个或多个空白(空格/制表符)」当作字段分隔符。也就是说,连续的空格会被当成一个分隔,不会切出空字段:
echo "Tom 25 male" | awk '{print $1}' # Tom
echo "Tom 25 male" | awk '{print $1, $2}' # Tom 25
echo "Tom 25 male" | awk '{print $0}' # Tom 25 male(整行)
这里用了管道把字符串喂给 awk。'Tom 25 male' 按空格分隔成三列:$1=Tom、$2=25、$3=male,$0 是整行。
1.3 指定分隔符:-F
很多文件不是用空格,而是用冒号(/etc/passwd)、逗号(CSV)等分隔。用 -F 指定分隔符:
awk -F: '{print $1}' /etc/passwd # 以冒号分隔,取第 1 列(用户名)
awk -F, '{print $2}' data.csv # 以逗号分隔,取第 2 列
-F: 表示用冒号当字段分隔符,-F, 用逗号。这是 awk 处理「有规律分隔符的文件」最常用的方式——比如 /etc/passwd 就是典型的冒号分隔格式。
二、awk 基本语法与字段变量
2.1 基本语法
awk 的基本调用方式:
awk '程序' 文件
awk '程序' file1 file2
command | awk '程序'
可以处理单个/多个文件,也可以从管道读输入。「程序」由模式(条件)+ 动作组成:当模式成立时,执行动作;没有写模式,则对每一行都执行动作。这是 awk 程序的基本单元。
2.2 最简单的形式:{ 动作 }
最简单的 awk 程序就是一对花括号里写动作,对每一行都执行:
awk '{print $1}' file # 打印每行第 1 列
awk '{print $1, $3}' file # 打印第 1、3 列
awk '{print $NF}' file # 打印最后一列(NF 是列数)
{print $1} 对每一行执行「打印第 1 列」;print $1, $3 用逗号分隔打印两列(逗号会让 awk 在两个输出字段间插入一个空格);$NF 里的 NF 是「当前行有多少列」,所以 $NF 就是最后一列——这一招在处理列数不固定的行时特别有用。
2.3 常用字段/变量
awk 自带一批有特殊含义的变量:
| 变量 | 含义 |
|---|---|
$0 |
整行 |
$1 到 $n |
第 1 到 n 列 |
NF |
当前行的字段(列)数 |
NR |
当前已处理的行号 |
FS |
字段分隔符(默认空格) |
OFS |
输出字段分隔符 |
RS |
记录分隔符(默认换行) |
ORS |
输出记录分隔符 |
其中 NF(Number of Fields,列数)和 NR(Number of Record,行号)最常用,第 5 章会专门细讲。
2.4 打印多列与拼接
awk '{print $1"-"$2}' file # 拼接:Tom-25
awk '{print $1, $2}' file # 逗号分隔(用 OFS 控制)
- 第一个:把
$1、$2直接挨着写并用-连起来,是字符串拼接,输出Tom-25; - 第二个:用逗号分隔,输出时两个字段间会插入输出分隔符(默认是一个空格,可用
OFS改变)。
拼接连字符和逗号分隔是两种不同行为:并排写是拼接(中间不加东西),加逗号是「用分隔符连接」。
三、BEGIN 与 END:处理前/后的钩子
3.1 什么是 BEGIN / END
awk 除了「逐行处理」,还提供两个特殊的「钩子」,让程序在处理开始前和结束后各执行一次:
BEGIN { ... }:在处理第一行之前执行一次;END { ... }:在处理最后一行之后执行一次。
这两个块最常用于初始化(比如打印表头、设置变量)、汇总统计(比如输出总和、行数)和输出标题/结论。
3.2 打印标题
awk 'BEGIN {print "用户名\tUID"} {print $1, $3}' /etc/passwd
BEGIN 块先打印一行表头(用户名\tUID,\t 是制表符),然后逐行块 {print $1, $3} 打印每行的第 1、3 列(用户名和 UID)。这样输出就有了清晰的表头。
3.3 统计汇总(END 里输出结果)
统计类需求几乎都用 END 输出最终结果:
# 累加第 2 列,最后输出总和
awk '{sum += $2} END {print "总和:", sum}' file
# 统计行数
awk 'END {print "总行数:", NR}' file
第一个:逐行把第 2 列累加到变量 sum,所有行处理完,END 里输出总和。第二个:NR(行号)在最后一行时正好等于总行数,直接 END 里打印。END + 累加变量 = 统计的标配。
3.4 BEGIN 里设置分隔符
-F 能指定分隔符,在 BEGIN 里设置 FS 变量效果相同:
awk 'BEGIN {FS=":"} {print $1}' /etc/passwd # 等价于 -F:
两种写法等价:-F: 是命令行快捷方式,BEGIN {FS=":"} 是把分隔符写进程序里。程序较长、分隔符逻辑复杂时,用 BEGIN 更灵活。
四、awk 的模式匹配与条件
4.1 按字段条件过滤
awk 的动作前可以加条件,只有条件为真的行才执行动作:
# 第 3 列 > 50 时打印第 1 列
awk '$3 > 50 {print $1}' file
# 第 3 列 == 100
awk '$3 == 100 {print $1}' file
# 第 1 列等于指定值
awk '$1 == "root" {print $0}' /etc/passwd
'$3 > 50 {print $1}' 的结构是「条件 + 空格 + 动作」:条件 $3 > 50 为真(第 3 列数值大于 50)才执行 {print $1}。数值比较用 >、<、==;字符串比较要给值加引号,如 $1 == "root"。
4.2 正则匹配
awk 里也能用正则(第 3 篇那套)。两种形式:匹配整行,或匹配某一列:
awk '/error/ {print}' app.log # 匹配包含 error 的行
awk '$1 ~ /^root/ {print}' file # 第 1 列以 root 开头
awk '$1 !~ /admin/ {print}' file # 第 1 列不包含 admin
/error/ {print}:/正则/单独作为条件时,匹配整行是否包含该模式;$1 ~ /^root/:~是「匹配」运算符,$1 ~ 正则表示「第 1 列匹配这个正则」;$1 !~ /admin/:!~是「不匹配」,表示「第 1 列不包含 admin」。
4.3 逻辑组合
多个条件可以用 &&(并且)和 ||(或者)组合:
awk '$3 > 50 && $4 < 100 {print}' file # 同时满足
awk '$1 == "a" || $1 == "b" {print}' file # 满足其一
&& 要求两边都为真,|| 只要一边为真即可。
4.4 BEGIN / END 也是模式
从本质上看,BEGIN、END、普通条件(如 $3 > 50)都是「模式」。awk 的工作机制是:逐行读入,对每一行判断「模式是否匹配」,匹配就执行对应的动作。BEGIN 是「在没有任何行之前匹配一次」的特殊模式,END 是「在所有行之后匹配一次」的特殊模式。理解了「模式 + 动作」这个统一框架,awk 的结构就通透了。
五、awk 的内建变量
awk 自带一批常用的内建变量,直接拿来用,不用自己定义:
| 变量 | 含义 | 例子 |
|---|---|---|
NF |
当前行字段数 | 打印每行列数 |
NR |
当前行号 | NR==1 只处理第 1 行 |
FNR |
当前文件的行号(多文件时) | 多文件区分 |
FS |
字段分隔符 | 默认空格 |
OFS |
输出字段分隔符 | 默认空格 |
RS |
记录分隔符 | 默认换行 |
FILENAME |
当前文件名 | 多文件处理 |
它们的典型用途:
# 跳过表头(NR>1 才处理)
awk 'NR>1 {print $1}' data.txt
# 打印最后一行
awk 'END {print $0}' file
# 每行打印行号和列数
awk '{print "行"NR" 列数"NF": "$0}' file
NR>1 {print $1}:NR是行号,第一行 NR=1。条件NR>1表示「跳过第 1 行」,常用于跳过数据文件的表头;END {print $0}:END里$0是最后一行,所以打印最后一行;- 第三个把
NR(行号)和NF(列数)直接拼进字符串输出。
NR 是累计行号,FNR 是当前文件内行号。处理多个文件时,
NR会一直累加不归零,FNR则每个文件重新从 1 开始。所以想「每个文件都从第 2 行开始处理」时用FNR>1,想「所有文件加起来跳过第一行」时用NR>1。
六、awk 中的变量、运算与格式化
6.1 自定义变量
awk 里可以定义自己的变量,而且不需要声明类型(变量会按需自动变成数字或字符串):
awk '{total += $2; count++} END {print "平均:", total/count}' file
这里 total 和 count 是自定义变量:每行把第 2 列累加到 total,count++ 计数行数;所有行处理完,END 里用 total/count 算出平均值。自定义变量是 awk 做统计的核心工具。
6.2 算术运算
awk 支持 + - * / % 和自增 ++、自减 --:
awk '{print $2 * 2}' file
awk '{print $1 + $2}' file
awk '{count++} END {print count}' file
第三个 count++ 每行加 1,END 输出总行数——这是「计数」的经典写法。
6.3 字符串拼接
awk 里字符串拼接很简单:直接并排放就是拼接:
awk '{print $1 ":" $3}' file # Tom:25
$1 ":" $3 把第 1 列、冒号、第 3 列接在一起,输出 Tom:25。(注意:这里用双引号包住冒号是为了把它当字面字符串;如果用逗号 $1, $3 则是插入分隔符。)
6.4 格式化输出:printf
用 print 输出格式是固定的,想要精确控制对齐、小数位,要用 printf(类似 C 语言的 printf):
# %s 字符串,%d 整数,%.2f 保留两位小数
awk '{printf "%-10s %5d\n", $1, $2}' file
awk '{printf "平均分: %.2f\n", $2}' file
格式串里的占位符控制输出样式:
%s:字符串;%d:整数;%.2f:保留两位小数的浮点数;%-10s:字符串左对齐并占 10 个字符宽;%5d:整数右对齐占 5 位——这两个配合起来就能做出对齐的报表;\n是换行。
printf不会像\n,否则所有输出会挤在同一行。
6.5 if / else 在 awk 里
awk 也能写 if/else 做更复杂的分支:
awk '{if ($2 >= 60) print $1" 及格"; else print $1" 不及格"}' file
对每一行判断第 2 列是否大于等于 60,满足打印「及格」,否则打印「不及格」。awk 的 if/else 语法和 C 语言几乎一样,这也再次说明 awk 是一门完整的编程语言。
七、awk 统计实战
统计是 awk 最拿手的领域,几个经典场景。
7.1 统计每个用户的登录 shell
awk -F: '{count[$7]++} END {for (s in count) print s, count[s]}' /etc/passwd
这是 awk 最经典的用法之一,拆开看:
count[$7]++:用第 7 列(shell 路径)作为键放进一个关联数组count,每遇到一次这个值就加 1。比如第一次遇到/bin/bash,count["/bin/bash"]变成 1,再遇到就 2、3...END {for (s in count) print s, count[s]}:所有行处理完,用for (s in count)遍历数组的每个键,打印「shell 和它的计数」。
结果就是「每个 shell 被多少用户使用」。数组[键]++ 的分组计数是 awk 统计的灵魂技巧,几乎所有「按某列分组统计」的需求都用它。
7.2 统计日志里每个 IP 的访问次数
awk '{print $1}' access.log | sort | uniq -c | sort -rn
access.log 每行第 1 列通常是客户端 IP。这条命令:awk 取出所有 IP → sort 排序(相同的 IP 聚在一起)→ uniq -c 去重并计数 → sort -rn 按计数降序。结果就是「每个 IP 的访问次数,从多到少排列」。
7.3 求和与平均
# 第 2 列总和
awk '{sum += $2} END {print "总和:", sum}' data.txt
# 平均值
awk '{sum += $2; n++} END {print "平均:", sum/n}' data.txt
# 最大值
awk 'NR==1 || $2>max {max=$2} END {print "最大:", max}' data.txt
- 总和:逐行累加
sum += $2,END输出; - 平均:同时累加
sum和计数n,END里sum/n; - 最大值:
NR==1 || $2>max的意思是「第一行强制记录,之后只有第 2 列比当前 max 大才更新」,最后END输出 max。注意 max 初始是空(awk 里当作 0),用NR==1先兜住第一行。
7.4 格式化报表
awk '{printf "%-8s %8.2f\n", $1, $2}' data.txt
用第 6 章学的 printf:第 1 列左对齐占 8 位,第 2 列右对齐、保留两位小数。这样数据会被格式化成整齐对齐的报表。
八、awk 与 grep、sed 的协作
8.1 三者分工
| 需求 | 用哪个 |
|---|---|
| 筛行(挑匹配的行) | grep |
| 改内容(替换、删、增) | sed |
| 取列、统计、计算、格式化 | awk |
记住这个分工,拿到需求先分类,就知道该用谁。
8.2 管道协作示例
三剑客经常用管道接力,各干各的强项:
# grep 筛行 + awk 取列统计
grep "ERROR" app.log | awk '{print $2}' | sort | uniq -c
# sed 清洗 + awk 统计
sed 's/,//g' data.csv | awk '{sum += $1} END {print sum}'
# awk 取列 + grep 再筛
awk '{print $1, $3}' file | grep "^root"
第一个:grep 筛出 ERROR 行 → awk 取第 2 列 → sort/uniq 统计;第二个:sed 先把逗号去掉 → awk 求和;第三个:awk 取两列 → grep 筛出以 root 开头的行。
8.3 经验
- 一条命令能用 awk 完成的事,优先用 awk(减少管道级数,效率更高、更清晰);
- grep 负责快速粗筛,sed 负责批量改,awk 负责精确计算——各司其职,按需组合。
九、awk 脚本文件与进阶
9.1 把 awk 程序写成文件
程序很长时,写成一行不现实。可以把 awk 程序放进一个 .awk 文件,用 -f 引用:
# report.awk
BEGIN {print "名称\t分数"}
{printf "%s\t%d\n", $1, $2}
END {print "--- 完成 ---"}
# 运行
awk -f report.awk data.txt
awk -f report.awk 从文件读程序。文件里可以写多个块(BEGIN/逐行/END),每条规则占一行或多行,可读性大大提升。适合复杂的统计报表。
9.2 awk 里的循环
awk 也支持 for、while 循环:
# for 循环遍历数组
awk '{count[$1]++} END {for (k in count) print k, count[k]}' file
# 逐字段循环
awk '{for (i=1; i<=NF; i++) sum += $i} END {print sum}' file
第一个:for (k in count) 遍历关联数组的键(第 7.1 节见过);第二个:for (i=1; i<=NF; i++) 从第 1 列循环到最后一列,把每一列都累加——处理「列数不固定的行」很有用。
9.3 awk 与脚本的配合
awk 常被嵌在 bash 脚本里做「文本统计」,然后用命令替换把结果交回脚本变量(第 1 篇的 $( )):
count=$(awk -F: '{print $1}' /etc/passwd | wc -l)
echo "用户数: $count"
awk 取出所有用户名,wc -l 数行数,命令替换 $(...) 把数字存进 count,脚本就能用了。awk 是脚本里做「文本统计」的主力,配合命令替换可以轻松把统计结果交回脚本。
十、总结与练习
10.1 核心速查
- awk 定位:按列处理、统计、格式化;
- 模型:记录(行)+ 字段(列),$0 整行、$1 第 1 列、NF 列数、NR 行号;
- 分隔符:-F 或 BEGIN 里设 FS;
- BEGIN/END:处理前/后各执行一次,常用于表头和汇总;
- 条件:$3>50、/regex/、$1 ~ /regex/、&&、||;
- 统计:关联数组 count[$x]++、sum += $2、END 里输出;
- 格式化:printf 控制格式;
- 与脚本:命令替换捕获 awk 结果。
10.2 动手练习
- 用 awk -F: 打印 /etc/passwd 的用户名和 UID(第 1、3 列);
- 统计 /etc/passwd 里每个 shell(第 7 列)的用户数;
- 计算一组数据(第 2 列)的总和和平均值;
- 用 printf 把数据格式化成对齐的报表;
- 在 bash 脚本里用 awk 统计日志某关键词并赋值给变量。
参考资料
- GNU awk 手册(man awk / gawk):https://www.gnu.org/software/gawk/manual/
- 菜鸟教程 - awk 命令:https://www.runoob.com/linux/linux-comm-awk.html
- awk 实用速查:https://www.gnu.org/software/gawk/manual/html_node/index.html
