Linux 文本处理三件套:grep、sed、awk 常用片段与小技巧

平时在服务器上排查问题、整理日志,十有八九绕不开 grep、sed、awk 这三个工具。它们各有侧重:grep 负责找行,sed 负责改行,awk 负责按列拆分和计算。这篇是我自己的备忘,把用得比较顺手的片段记下来,顺便写清楚每条命令在做什么,免得过段时间又忘了参数。

grep:把符合条件的行挑出来

grep 最基础的用法就是在文件里找字符串。下面几个选项基本能覆盖八成场景。

# 在文件里查找包含 error 的行,-i 忽略大小写
grep -i "error" app.log

# -n 显示行号,定位起来方便
grep -n "timeout" app.log

# -r 递归整个目录,--include 限定只看 .go 文件
grep -rn --include="*.go" "TODO" ./src

# -v 反向,排除掉注释行和空行
grep -v "^#" config.ini | grep -v "^$"

查问题时最常用的是看上下文,光看命中那一行往往不够:

# -A 看后面 3 行,-B 看前面 3 行,-C 是前后都看
grep -C 3 "panic" app.log

需要正则的时候,建议直接上 -E(扩展正则),省得为了 +?| 这些到处加反斜杠:

# 匹配 4xx 或 5xx 状态码,\b 是单词边界
grep -E "\b[45][0-9]{2}\b" access.log

# -o 只输出匹配到的部分,而不是整行。配合后续统计很好用
grep -oE "[0-9]{1,3}(\.[0-9]{1,3}){3}" access.log

一个容易被忽略的点:grep -F 表示把模式当成普通字符串处理,不走正则。当你要找的内容里本身带 .* 这类字符时,加上 -F 能避免误匹配,而且速度也更快。

sed:逐行做替换和编辑

sed 最高频的操作就是替换。记住 s/旧/新/ 这个结构,基本就够日常用了。

# 把每行第一个 foo 替换成 bar,结果打到屏幕(不改原文件)
sed 's/foo/bar/' notes.txt

# 末尾加 g 表示一行内全部替换,而不只是第一个
sed 's/foo/bar/g' notes.txt

# -i 原地修改文件。强烈建议先用 .bak 留个备份
sed -i.bak 's/foo/bar/g' notes.txt

关于 -i 有个常见的坑:GNU sed(多数 Linux)写 sed -i 即可,而 BSD sed(macOS 自带)的 -i 后面必须跟一个备份后缀参数,哪怕是空字符串。为了脚本能跨平台跑,我习惯统一写成 sed -i.bak,事后再删掉 .bak 文件,这样两边都不会报错。

替换里如果内容带斜杠,可以换个分隔符,读起来清爽很多:

# 用 | 当分隔符,处理路径时不用转义 /
sed 's|/usr/local|/opt|g' paths.txt

除了替换,sed 还能按行号或匹配做删除、打印:

# 删除所有空行
sed '/^$/d' draft.txt

# 删除以 # 开头的注释行
sed '/^#/d' config.ini

# 只打印第 10 到 20 行(-n 关掉默认输出,p 表示打印)
sed -n '10,20p' app.log

# 在匹配到 [server] 的行后面插入一行
sed '/\[server\]/a port = 8080' config.ini

小技巧:想看某个时间段的日志,用两个匹配做范围截取很方便。下面这条会打印从第一次出现 09:00 到第一次出现 10:00 之间的所有行。

sed -n '/09:00/,/10:00/p' app.log

awk:按字段拆分和统计

awk 的核心是把每一行按分隔符拆成 $1$2 这些字段,$0 是整行,NF 是字段总数,NR 是当前行号。理解了这几个变量,大部分需求都能拼出来。

# 默认按空白分隔,打印第 1 列和第 3 列
awk '{print $1, $3}' access.log

# -F 指定分隔符,这里按冒号拆分 passwd
awk -F: '{print $1, $7}' /etc/passwd

# 打印每行最后一个字段,$NF 就是最后一列
awk '{print $NF}' app.log

awk 自带条件判断,可以边过滤边输出,经常能替代 grep 加管道的组合:

# 只打印第 9 列(状态码)大于等于 400 的行
awk '$9 >= 400' access.log

# 第 3 列等于 ERROR 时,打印行号和整行
awk '$3 == "ERROR" {print NR, $0}' app.log

统计是 awk 最出彩的地方。BEGIN 块在处理数据前执行,END 块在全部读完后执行,中间用变量累加即可。

# 对第 5 列求和,处理完在 END 里输出总和
awk '{sum += $5} END {print sum}' data.txt

# 求平均值,NR 在 END 里正好是总行数
awk '{sum += $5} END {print sum / NR}' data.txt

用关联数组做分组计数,是日志分析里特别实用的一招。比如统计访问量最高的 IP:

# 以第 1 列为 key 累加,END 里遍历数组输出
awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' access.log \
  | sort -rn | head

这条命令把每个 IP 出现的次数记进 count 数组,读完后输出“次数 IP”,再交给 sort -rn 按数字倒序排,head 取前几名。把 $1 换成状态码那一列,就成了状态码分布统计,思路完全一样。

三者配合的几个组合

真实场景里很少单用一个工具,管道串起来才好使。这里举两个我常敲的组合。

# 统计某个接口各状态码出现的次数
grep "/api/login" access.log | awk '{print $9}' | sort | uniq -c | sort -rn

这里 grep 先把目标接口的行筛出来,awk 取出状态码那一列,sortuniq -c 完成去重计数,最后再排序。uniq -c 有个前提:输入必须先排好序,所以前面那个 sort 不能省。

# 提取日志里的所有 IP,去重后看总数
grep -oE "[0-9]{1,3}(\.[0-9]{1,3}){3}" access.log | sort -u | wc -l

grep -o 把每个 IP 单独抠出来,sort -u 排序加去重,wc -l 数行数,一条命令就知道有多少个独立来源。

几条顺手的小提醒

这三件套学起来没什么门槛,难的是把它们组合成顺手的管道。记住各自的定位:grep 找、sed 改、awk 算,遇到新需求时按这个分工去拆,大多数文本处理任务都能在一行命令里搞定。