Linux 文本处理利器:AWK 从入门到实战
在 Linux 命令行里,awk 是最值得掌握的文本处理工具之一。
它既是文本扫描器,也是一个小型编程语言。对日志、表格、简单分隔文件、命令输出这类“按行组织、按列取值”的数据,awk 往往比通用脚本语言更直接,也比单纯的正则替换工具更适合。
如果任务只是简单替换一段文本,sed 通常更短;如果任务需要“按列取值、带条件筛选、顺手再做统计”,awk 基本就是更合适的工具。
什么时候该用 AWK,而不是 sed
结论很直接:
- 处理整行替换、删除、插入,优先考虑
sed - 处理字段、列、条件、统计、报表,优先考虑
awk
两者都面向文本流,但设计目标不同。
| 维度 | awk |
sed |
|---|---|---|
| 主要用途 | 模式扫描与数据处理 | 流式编辑与文本替换 |
| 处理单位 | 行 + 字段 | 行 |
| 编程能力 | 变量、条件、循环、数组 | 有限脚本能力 |
| 适合场景 | 日志、表格、CSV 风格数据 | 查找替换、行编辑 |
| 复杂任务可读性 | 较高 | 通常较低 |
比如,把文件中所有 error 替换成 warning:
sed 's/error/warning/g' file.txt
这类任务 sed 更自然。
但如果只想打印第 1 列和第 3 列:
awk '{print $1, $3}' file.txt
这就是 awk 的主场。
再看一个对比:如果只取每行第一列,awk 写法很直白:
awk '{print $1}' file.txt
对应的 sed 写法会更绕:
sed 's/^\([^ ]*\).*/\1/' file.txt
两个命令都能完成任务,但字段型数据处理里,awk 可读性明显更好。
AWK 的基本语法是什么,最小可用形式怎么写
awk 的核心模型是:
awk '/search_pattern/ { action_to_take_on_matches; another_action; }' file_to_parse
可以把它理解成“匹配条件 + 执行动作”。
有三个常见简化:
- 只有模式,没有动作:默认动作是
print - 只有动作,没有模式:对每一行都执行
- 模式和动作都写:只对匹配行执行动作
先准备一个示例文件:
echo "carrot sandy
wasabi luke
sandwich brian
salad ryan
spaghetti jessica" > favorite_food.txt
直接打印整个文件:
awk '{print}' favorite_food.txt
输出:
carrot sandy
wasabi luke
sandwich brian
salad ryan
spaghetti jessica
这相当于把 awk 当成 cat 用,没什么技术含量,但能说明默认处理方式就是逐行扫描。
按内容过滤,查找包含 sand 的行:
awk '/sand/' favorite_food.txt
输出:
carrot sandy
sandwich brian
如果只匹配行首是 sand 的内容:
awk '/^sand/' favorite_food.txt
输出:
sandwich brian
再加动作,只打印匹配行的第一列:
awk '/^sand/ {print $1;}' favorite_food.txt
输出:
sandwich
这里已经出现了 awk 最重要的字段变量:
$1:第 1 列$2:第 2 列$3:第 3 列$0:整行
默认情况下,awk 用空白字符分隔字段,包括空格和制表符。
AWK 的内置变量能解决什么问题
awk 的强大不只是 $1、$2 这种字段引用,还在于一组内置变量。这些变量让脚本具备“知道自己正在处理什么”的能力。
常用内置变量如下:
FILENAME:当前输入文件名FNR:当前文件中的记录号FS:输入字段分隔符NF:当前行字段数NR:全局记录号OFS:输出字段分隔符ORS:输出记录分隔符RS:输入记录分隔符
如何输出行号和字段数
打印行号和整行:
awk '{print NR, $0}' file.txt
打印每行字段数:
awk '{print "Fields:", NF}' file.txt
这些变量在调试输入格式时很有用。碰到“为什么这一行没匹配”“为什么字段数不对”的情况,先看 NF 和 NR,通常能很快定位问题。
BEGIN 和 END 该怎么用
awk 还有两个很关键的特殊块:
BEGIN:处理输入前执行一次END:处理完成后执行一次
完整结构通常写成:
awk 'BEGIN { action; }
/search/ { action; }
END { action; }' input_file
适用原则很简单:
- 初始化分隔符、打印表头,放
BEGIN - 输出统计结果、收尾说明,放
END
为什么要在 BEGIN 里设置 FS
像 /etc/passwd 这种文件不是空白分隔,而是冒号分隔。处理这类文件时,应该先设定 FS:
awk 'BEGIN { FS=":"; }
{ print $1; }' /etc/passwd
输出类似:
root
daemon
bin
sys
sync
games
man
. . .
也可以直接用 -F,后面会讲。两者都行;如果脚本稍微复杂一点,放在 BEGIN 里更集中。
如何用 BEGIN 和 END 生成带表头的输出
下面这个例子把 /etc/passwd 整理成一个带标题的表格:
awk 'BEGIN { FS=":"; print "User\t\tUID\t\tGID\t\tHome\t\tShell\n--------------"; }
{print $1,"\t\t",$3,"\t\t",$4,"\t\t",$6,"\t\t",$7;}
END { print "---------\nFile Complete" }' /etc/passwd
输出类似:
User UID GID Home Shell
--------------
root 0 0 /root /bin/bash
daemon 1 1 /usr/sbin /bin/sh
bin 2 2 /bin /bin/sh
sys 3 3 /dev /bin/sh
sync 4 65534 /bin /bin/sync
. . .
---------
File Complete
如果只是想执行一次初始化逻辑,甚至可以完全不读文件:
awk 'BEGIN { print "We can use awk like the echo command"; }'
输出:
We can use awk like the echo command
如何只在某一列上做匹配,而不是匹配整行
很多人刚开始用 awk 时,容易把它当成“会打印字段的 grep”。这会导致一个常见问题:本来想匹配某一列,却误匹配了整行中别的位置。
先准备新的示例文件:
echo "1 carrot sandy
2 wasabi luke
3 sandwich brian
4 salad ryan
5 spaghetti jessica" > favorite_food.txt
如果直接这样查找:
awk '/sa/' favorite_food.txt
输出会是:
1 carrot sandy
2 wasabi luke
3 sandwich brian
4 salad ryan
因为它匹配的是整行里任意位置出现的 sa,所以:
wasabi会被匹配sandy也会被匹配- 这不等于“第二列以
sa开头”
如何对字段使用正则匹配
只匹配第二列以 sa 开头:
awk '$2 ~ /^sa/' favorite_food.txt
输出:
3 sandwich brian
4 salad ryan
这里的关键语法是:
~:字段匹配正则$2 ~ /^sa/:第二列匹配以sa开头
反向匹配则用 !~:
awk '$2 !~ /^sa/' favorite_food.txt
输出:
1 carrot sandy
2 wasabi luke
5 spaghetti jessica
如何组合多个条件
如果要求“第二列不以 sa 开头,并且第一列小于 5”:
awk '$2 !~ /^sa/ && $1 < 5' favorite_food.txt
输出:
1 carrot sandy
2 wasabi luke
这里用到了复合表达式:
&&:与- 可以组合任意多个条件
- 数值比较和正则匹配可以混用
这也是 awk 和单纯文本过滤工具真正拉开差距的地方:它天然适合“基于列的条件判断”。
关联数组为什么是 AWK 最有价值的能力之一
如果只做过滤和打印,awk 已经很好用;但真正让它进入“轻量数据处理引擎”范围的,是关联数组。
awk 的数组不是只能用数字下标,还可以用字符串做键。这很适合做:
- 计数
- 分组
- 累加
- 汇总
这些操作都可以在单次扫描里完成,不一定需要额外接 sort/uniq。
如何用关联数组做计数
先准备数据:
echo "apple
banana
apple
orange
banana
apple" > fruits.txt
统计每种水果出现次数:
awk '{count[$1]++} END {for (item in count) print item, count[item]}' fruits.txt
输出类似:
apple 3
banana 2
orange 1
这个模式非常常见:
count[$1]++:以第一列值为键做计数END中统一输出结果
要注意一点:for (item in count) 的输出顺序通常不保证稳定。如果需要排序,后面接 sort。
如何按字段分组
准备数据:
echo "HR Alice
IT Bob
HR Charlie
IT David
Finance Emma" > employees.txt
按部门分组员工:
awk '{group[$1] = (group[$1] ? group[$1] " " : "") $2} END {for (dept in group) print dept ": " group[dept]}' employees.txt
输出类似:
HR: Alice Charlie
IT: Bob David
Finance: Emma
这里用了一个常见技巧:
(group[$1] ? group[$1] " " : "") $2
作用是:
- 如果当前组已经有值,就先拼一个空格再追加
- 如果还没有值,就直接写入第一个名字
- 这样可以避免输出字符串前面多一个空格
如何做累加求和
准备销售数据:
echo "Alice 100
Bob 200
Alice 150
Bob 50" > sales.txt
按人汇总销售额:
awk '{sum[$1] += $2} END {for (name in sum) print name, sum[name]}' sales.txt
输出:
Alice 250
Bob 250
模式也很固定:
$1做键$2做累加值
如何找出最大值
在汇总后继续扫描数组,找出总值最大的人:
awk '{sum[$1] += $2} END {
for (name in sum) {
if (top == "" || sum[name] > max) { max = sum[name]; top = name }
}
print "Top performer:", top, max
}' sales.txt
这种写法适合做简单分析:最大值、最小值、热点来源、错误最多的服务名,都能用同样思路处理。
AWK 如何处理其他命令的输出
awk 不一定非要读文件。命令输出通过管道喂给它,往往才是日常使用里的高频场景。
比如解析 ip 命令输出,提取 eth0 的 IPv4 地址。
先看原始命令:
ip a s eth0
输出类似:
2571: eth0@if2572: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:0b brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.11/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
提取 IP:
ip a s eth0 | awk -F '[\/ ]+' '/inet / {print $3}'
输出:
172.17.0.11
这里 -F '[\/ ]+' 的意思是把斜杠和空格都当作分隔符。
因此 inet 172.17.0.11/16 会被拆成多个字段,而 IP 地址落在 $3。
这种写法对命令行排障很实用:命令先产生结构化文本,awk 负责抽取关心的字段。
在脚本和自动化里,AWK 该怎么嵌入
awk 最适合的不是临时一条命令,而是逐渐沉淀成稳定脚本。
适用场景包括:
- 周期性日志统计
- 批量文件扫描
- 系统信息提取
- 简单报表生成
如何在 Bash 脚本里直接使用 AWK
一个最简单的例子,从 /etc/passwd 提取用户名:
#!/bin/bash
awk -F ":" '{print $1}' /etc/passwd
赋予可执行权限并运行:
chmod +x extract_users.sh
./extract_users.sh
这里 -F ":" 用于设置字段分隔符。对短命令而言,这比在 BEGIN 中写 FS=":" 更紧凑。
Shell 变量应该怎么传给 AWK
结论很明确:优先用 -v。
示例:
#!/bin/bash
threshold=100
awk -v limit="$threshold" '$2 > limit {print $1, $2}' sales.txt
这里把 Bash 变量 threshold 传给 awk 中的 limit。
这样写的好处是:
- 引号处理更安全
- 变量里有空格或特殊字符时不容易炸
- 比把 shell 变量直接拼进 awk 程序更稳
批量处理多个文件时怎么写
#!/bin/bash
for file in *.log; do
echo "Processing $file"
awk '/ERROR/ {print $0}' "$file"
done
这个脚本会遍历当前目录下所有 .log 文件,提取包含 ERROR 的行。
如果使用 Bash,且希望在没有匹配文件时循环直接跳过,可以启用:
shopt -s nullglob
这样 *.log 在无匹配时会展开为空,而不是字面量字符串 *.log。
如何把 AWK 用在日志自动分析里
比如统计一个日志文件中 ERROR 的数量:
#!/bin/bash
logfile="app.log"
awk '/ERROR/ {count++} END {print "Total errors:", count+0}' "$logfile"
这里的 count+0 很实用。
如果没有任何匹配,count 仍可能是空值;加上 0 后,输出会稳定为数字 0。
AWK 是否应该和其他 Unix 工具混用
应该,而且很多时候混用更清楚。
例如统计访问日志中触发 404 的 IP 次数:
grep "404" access.log | awk '{print $1}' | sort | uniq -c
这条管道分成四步:
grep过滤404awk取第一列 IPsort排序uniq -c计数
严格说,有些步骤也可以并进 awk,但拆开之后可读性往往更好,维护成本也更低。命令行工具不需要为了“全用一个命令”而故意写复杂。
真实场景里,AWK 最适合处理哪些数据
结论是:适合规则明确、按行组织、字段边界清晰的数据。
典型包括:
- Web 访问日志
- 系统账户文件
- 简单 CSV
- 命令输出
- 空白分隔表格
不太适合的场景也要明确:
- 带引号和转义规则的复杂 CSV
- 深层嵌套结构
- 需要长逻辑和复杂错误处理的脚本
这类任务切到 Python、专用解析器或结构化工具更合适。
如何解析 Apache 访问日志
假设日志格式类似:
127.0.0.1 - - [10/Oct/2023:13:55:36 +0000] "GET /index.html HTTP/1.1" 200 2326
提取 IP 和 HTTP 状态码:
awk '{print $1, $9}' access.log
在默认常见格式下:
$1是客户端 IP$9是状态码
统计 404 数量:
awk '$9 == 404 {count++} END {print "404 errors:", count+0}' access.log
输出即使无匹配,也会是 0。
AWK 能不能处理 CSV
能,但前提要讲清楚:只适合简单 CSV。
例如:
name,age,city
Alice,30,New York
Bob,25,London
Charlie,35,Sydney
打印姓名和城市:
awk -F ',' '{print $1, $3}' data.csv
按年龄过滤大于 30 的记录:
awk -F ',' '$2 > 30 {print $1, $2}' data.csv
限制条件是:
- 字段里不能有带引号的逗号
- 没有复杂转义规则
如果 CSV 中存在 "New York, USA" 这种字段,普通 awk -F ',' 就不可靠了,应该改用 CSV 感知的解析器。
如何分析系统文件 /etc/passwd
列出 UID 大于 1000 的用户:
awk -F ':' '$3 > 1000 {print $1, $3}' /etc/passwd
统计这类用户数量:
awk -F ':' '$3 > 1000 {count++} END {print count+0}' /etc/passwd
这类系统文件格式稳定,非常适合用 awk 做快速检查。
如何快速汇总日志来源分布
统计每个 IP 发起了多少次请求:
awk '{count[$1]++} END {for (ip in count) print ip, count[ip]}' access.log
如果需要稳定排序,可以接 sort:
awk '{count[$1]++} END {for (ip in count) print ip, count[ip]}' access.log | sort
常见问题:AWK、gawk、mawk 有什么区别
awk 是 POSIX 定义的语言和命令接口。
gawk 是 GNU 的实现,很多 Linux 发行版默认提供它,并带有额外扩展。
mawk 是另一种实现,通常更强调速度和较低开销,但未必支持所有 GNU 扩展。
如果只写标准 awk 语法,可移植性最好。
常见问题:如何指定字段分隔符
最常见的方式是 -F:
awk -F ':' '{print $1}' /etc/passwd
也可以在 BEGIN 里设:
awk 'BEGIN { FS=":" } { print $1 }' /etc/passwd
短命令优先 -F,稍复杂脚本用 BEGIN 统一初始化也很合理。
常见问题:如何统计某个值出现的次数
直接用关联数组:
awk '{count[$1]++} END {for (w in count) print w, count[w]}' file.txt
这是 awk 最常用的统计模式之一。
常见问题:AWK 能处理多行记录吗
能。把记录分隔符 RS 设为空字符串即可:
RS=""
这样 awk 会把“空行分隔的一整块文本”视为一条记录,适合处理段落型输入。
常见问题:如何把 Shell 变量传入 AWK
推荐:
threshold=100
awk -v t="$threshold" '$3 > t {print $0}' file.txt
不要优先考虑字符串拼接,那会让引号和转义问题变得很烦。
常见问题:AWK 适合处理大文件吗
适合。
awk 按记录流式处理输入,不需要一次把整个文件读进内存。对大多数日志、表格、系统文本,性能和内存占用都足够好。
但如果逻辑已经复杂到接近一个中型程序,继续堆在 awk 里通常不划算。那时应该考虑 Python 等更易维护的方案。
常见问题:复杂 AWK 程序怎么保存和运行
可以把程序写进文件,然后用 -f 执行:
awk -f script.awk input.txt
当规则、函数、BEGIN/END 块逐渐变多时,这比把所有逻辑塞进一行命令更可维护。
该怎么建立一套稳定的 AWK 使用习惯
如果日常工作里经常碰到日志、命令输出和系统文本,下面这套习惯基本够用:
- 先判断任务是“行替换”还是“字段处理”
- 字段处理优先上
awk - 明确输入分隔符,必要时先设
FS - 用
$1、$2、NF、NR先看清数据结构 - 简单筛选用模式,复杂条件写表达式
- 需要汇总时直接上关联数组
- shell 变量传值统一用
-v - 输出顺序有要求时,不要假设数组遍历有序,配合
sort
awk 的价值不在于语法多,而在于它能把大量“原本要写个小脚本”的任务压缩成一条可读、可复用、可组合的命令。
对结构化文本处理来说,这个工具一直不过时。
关于
关注我获取更多资讯