Linux 文本处理利器:AWK 从入门到实战

系统梳理 AWK 的语法、字段处理、内置变量、关联数组、管道协作与脚本自动化用法,适合用来处理日志、表格和分隔文本。

阅读时长: 10 分钟
共 4608字
作者: eimoon.com

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

这些变量在调试输入格式时很有用。碰到“为什么这一行没匹配”“为什么字段数不对”的情况,先看 NFNR,通常能很快定位问题。

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

这条管道分成四步:

  1. grep 过滤 404
  2. awk 取第一列 IP
  3. sort 排序
  4. 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 使用习惯

如果日常工作里经常碰到日志、命令输出和系统文本,下面这套习惯基本够用:

  1. 先判断任务是“行替换”还是“字段处理”
  2. 字段处理优先上 awk
  3. 明确输入分隔符,必要时先设 FS
  4. $1$2NFNR 先看清数据结构
  5. 简单筛选用模式,复杂条件写表达式
  6. 需要汇总时直接上关联数组
  7. shell 变量传值统一用 -v
  8. 输出顺序有要求时,不要假设数组遍历有序,配合 sort

awk 的价值不在于语法多,而在于它能把大量“原本要写个小脚本”的任务压缩成一条可读、可复用、可组合的命令。

对结构化文本处理来说,这个工具一直不过时。

关于

关注我获取更多资讯

月球基地博客公众号二维码,扫码关注获取更多 AI 与编程资讯
📢 公众号
月球基地博客作者个人微信二维码,扫码交流 AI 与编程话题
💬 个人号
使用 Hugo 构建
主题 StackJimmy 设计