/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
引言:一次凌晨的告警,让我真正认识了“三剑客”
那是某电商大促前夜,凌晨两点,运维群里突然炸出一条告警:某核心支付网关的错误日志在 10 分钟内暴涨到 2 万行。值班同事在群里喊了一句——“谁帮我从日志里把超时订单号捞出来?”我迷迷糊糊打开服务器,面对几个 GB 的 error.log,心里只有一个念头:千万别想着用鼠标滚轮翻。
于是三行命令下去:
grep -c "TIMEOUT" /var/log/pay/error.log # 先看超时出现了多少次
grep "TIMEOUT" /var/log/pay/error.log | sed -n 's/.*orderId=\\([0-9]*\\).*/\\1/p' | sort | uniq -c | sort -rn | head -20
awk '$3 > 5000 {print $1, $5, $7}' access.log # 捞出响应超时 5 秒以上的请求
不到五分钟,问题定位:某个下游渠道接口抖动,导致大量订单超时重试。这一晚之后,我彻底明白了一个道理——在 Linux 服务器上,不会用 grep、sed、awk 处理文本,就像带着算盘去参加编程竞赛。它们被称为“文本处理三剑客”,是运维、后端、数据工程师日常工作中绕不开的基本功。
本文将从三剑客的定位差异讲起,逐一带你吃透 grep、sed、awk 的核心语法、常用参数、实战案例与避坑指南,并在结尾准备了一套练习题帮你巩固。无论你是刚接触 Linux 的新人,还是想系统梳理的老手,相信都会有所收获。
一、三剑客总览:各司其职,组合无敌
在深入细节之前,先用一张表格建立全局认知。很多初学者把三者混为一谈,实际上它们的分工非常清晰:
| grep | 文本搜索 | 从文件或数据流中“挑出”包含指定模式的行 | 查日志、过滤关键词、统计出现次数 |
| sed | 流编辑 | 按行对文本进行“查找、替换、删除、插入”等操作 | 批量替换配置、删除空行、提取区间行 |
| awk | 文本分析 | 按字段切分每一行,做判断、计算与格式化输出 | 日志统计、报表生成、多列条件筛选 |
打个比方:如果文本是一片海滩,grep 是金属探测器,专门帮你找出埋着“关键字”的位置;sed 是推土机,可以成片地替换、删除、重塑地形;awk 则是精密的筛分仪,能把沙粒按大小、重量分类统计,最终生成报表。
三者还有一个重要共性:都基于“行”处理,都支持从标准输入读取数据,因此可以无缝衔接管道 |,形成威力巨大的组合拳。后面我们会看到大量组合示例。
二、grep:文本搜索的利器
2.1 grep 是什么
grep 的全称是 Global Regular Expression Print,即“全局正则表达式打印”。它的职责很简单:在文件或标准输入中,逐行扫描,把包含指定模式的行打印出来。
2.2 基本语法
grep [选项] 模式 [文件...]
最常用的选项:
| -i | 忽略大小写 |
| -v | 反向匹配,输出不包含模式的行 |
| -n | 显示行号 |
| -c | 只输出匹配行数,不输出内容 |
| -r | 递归搜索目录 |
| -E | 使用扩展正则表达式(等价于 egrep) |
| -o | 只输出匹配到的部分,而不是整行 |
| -A n | 同时输出匹配行之后的 n 行(After) |
| -B n | 同时输出匹配行之前的 n 行(Before) |
| -C n | 同时输出前后各 n 行(Context) |
2.3 基础示例
假设有一个测试文件 test.txt,内容如下:
Hello World
hello linux
HELLO grep
Goodbye World
this is a test line
Linux is awesome
示例 1:忽略大小写查找 hello
grep -i "hello" test.txt
预期输出:
Hello World
hello linux
HELLO grep
示例 2:显示行号并反向匹配
grep -vn "World" test.txt
预期输出:
2:hello linux
3:HELLO grep
5:this is a test line
6:Linux is awesome
这里 -v 排除了包含 World 的行,-n 给每行加上了行号。
示例 3:统计错误日志中某关键词出现次数
grep -c "ERROR" /var/log/application.log
预期输出(示例值):
137
2.4 进阶用法:正则表达式
grep 的强大之处在于支持正则表达式。默认使用基础正则(BRE),加 -E 后使用扩展正则(ERE),后者更接近大多数人的书写习惯。
示例 4:用 -E 匹配多个关键词(逻辑或)
grep -E "ERROR|FATAL" app.log
这行命令会同时匹配包含 ERROR 或 FATAL 的行。
示例 5:用 -o 只提取 IP 地址
grep -oE "([0-9]{1,3}\\.){3}[0-9]{1,3}" access.log | sort | uniq -c | sort -rn | head -5
预期含义:从访问日志中提取所有 IP,统计出现频次,按从高到低取前 5 名。这是分析“谁在猛刷接口”的经典操作。
示例 6:结合 -A/-B/-C 看上下文
当某行报错时,只看这一行往往不够,需要看它前后的调用链:
grep -n -C 3 "NullPointerException" app.log
预期输出会给出每个匹配行前后各 3 行内容,方便追溯异常源头。
2.5 注意事项与常见错误
- 忘加引号:grep hello world file.txt 会被解释为在 world 和 file.txt 两个文件中查找 hello,而不是查找字符串 hello world。包含空格或特殊符号的模式必须用引号包裹。
- 正则“贪婪”与特殊字符:.、*、[ 等字符在正则中有特殊含义,想匹配字面量需要用 \\.、\\*、\\[ 转义。
- 管道后的 grep 没有行号意义:cat file | grep -n xxx 中的 -n 行号是相对“管道流”的,如果只取了部分数据,行号可能不是原文件行号,排查问题时注意区分。
- grep -r 在符号链接目录中的行为:递归搜索时,某些版本不会自动跟随符号链接,需要加 -R(注意大小写)来跟随。
三、sed:流编辑器
3.1 sed 是什么
sed 是 Stream Editor(流编辑器)的缩写。它把输入流逐行读入“模式空间”,根据脚本命令对该行做编辑,再把结果输出。默认情况下,sed 不会修改原文件,而是把结果输出到标准输出;只有加上 -i 选项才会直接改文件。
3.2 基本语法
sed [选项] '命令' 文件
常用选项:
| -n | 不自动打印每一行(配合 p 命令精确输出) |
| -i | 直接修改原文件(危险操作,建议先备份) |
| -e | 允许多个命令 |
| -r / -E | 使用扩展正则表达式 |
常用命令(写在单引号内的部分):
| p | 打印行 |
| d | 删除行 |
| s/旧/新/ | 替换(每行替换第一处) |
| s/旧/新/g | 全局替换(替换所有匹配处) |
| a | 在匹配行后追加文本 |
| i | 在匹配行前插入文本 |
地址限定:可以指定“对哪些行操作”:
| 3p | 只打印第 3 行 |
| 1,5d | 删除第 1 到 5 行 |
| /模式/p | 打印匹配模式的行 |
| $ | 最后一行 |
3.3 典型示例
沿用上面的 test.txt:
Hello World
hello linux
HELLO grep
Goodbye World
this is a test line
Linux is awesome
示例 1:删除空行与注释行
配置文件里常有讨厌的空行和 # 注释,干扰阅读:
sed -e '/^$/d' -e '/^#/d' nginx.conf
预期输出:所有空行和以 # 开头的行都被剔除。
示例 2:把每行第一处小写 hello 替换为 HI
sed 's/hello/HI/' test.txt
预期输出:
Hello World
HI linux
HELLO grep
Goodbye World
this is a test line
Linux is awesome
注意第 1 行是 Hello(首字母大写),没有被替换;第 3 行是 HELLO(全大写),也没有被替换。这是因为 sed 默认区分大小写,且每行只替换第一处。
示例 3:全局替换 + 忽略大小写
sed 's/hello/HI/gI' test.txt
预期输出:
HI world
HI linux
HI grep
Goodbye World
this is a test line
Linux is awesome
其中 g 表示全局替换,I 表示忽略大小写(GNU sed 支持)。
示例 4:区间操作——打印第 2 到第 5 行
sed -n '2,5p' test.txt
预期输出:
hello linux
HELLO grep
Goodbye World
this is a test line
示例 5:在匹配行后追加内容
sed '/Linux/a — 这是一条追加的分隔线 —' test.txt
预期输出:在包含 Linux 的每一行后面加一行分隔线。
示例 6:批量修改服务器配置(实战)
假设要把一批服务器的 Redis 配置从无密码改为有密码,并注释掉旧行:
# 先备份,再替换
cp redis.conf redis.conf.bak
sed -i 's/^# requirepass.*/requirepass MyStr0ng#Pass/' redis.conf
这里 ^# requirepass.* 匹配“以 # 开头、后跟 requirepass”的注释行,将其替换为正式的 requirepass 配置。注意 -i 会直接修改文件,所以务必先备份。
3.4 注意事项与常见错误
- -i 是一把双刃剑:sed -i 直接改原文件且不留备份,一旦写错正则很难恢复。推荐写法:sed -i.bak 's/old/new/' file,这样会生成 file.bak 作为备份。
- 分隔符不一定是 /:当模式或替换内容本身含 / 时(比如路径 /usr/local),可以用 #、|、: 等作分隔符,例如 sed 's#/usr/local#/opt#g' file,避免转义地狱。
- -n 与 p 的配合:很多人只加 p 不加 -n,结果每行被打印两次(一次是默认输出,一次是 p 命令输出)。想要只输出目标行,必须 -n 与 p 成对出现。
- sed 默认逐行处理:多行之间做替换(比如把两行合并成一行)比单行替换复杂得多,需要用到 N 命令等高级特性,初学阶段不要硬刚。
四、awk:文本分析与报表生成
4.1 awk 是什么
awk 的名字取自三位创始人 Alfred Aho、Peter Weinberger、Brian Kernighan 姓氏的首字母。它是一门模式扫描与处理语言,特别擅长把每一行按“字段”切开,然后做条件判断、数值计算和格式化输出。
如果把 grep 比作“挑选”,sed 比作“改写”,那么 awk 就是“问询与演说”——你可以一边对数据提问(某个字段是否大于阈值),一边生成漂亮的报表。
4.2 基本语法
awk '模式 {动作}' 文件
awk 处理每一行时:
内置变量速查:
| $0 | 当前整行内容 |
| $1,$2,… | 第 1、2……个字段 |
| NF | 当前行的字段数(Number of Fields) |
| NR | 当前已读的行号(Number of Records) |
| FS | 输入字段分隔符(默认空白) |
| OFS | 输出字段分隔符(默认空格) |
| RS | 输入记录分隔符(默认换行) |
4.3 典型示例
准备一个访问日志片段 access.log(字段:IP、时间、请求方法、状态码、响应耗时毫秒):
192.168.1.10 10:00:01 GET 200 120
192.168.1.11 10:00:02 POST 500 5200
192.168.1.12 10:00:03 GET 404 80
192.168.1.10 10:00:04 GET 200 3100
192.168.1.11 10:00:05 POST 200 90
示例 1:打印指定字段
只提取 IP 与方法:
awk '{print $1, $3}' access.log
预期输出:
192.168.1.10 GET
192.168.1.11 POST
192.168.1.12 GET
192.168.1.10 GET
192.168.1.11 POST
示例 2:按条件过滤——找出耗时超过 3 秒的请求
awk '$5 > 3000 {print $1, $3, $5 "ms"}' access.log
预期输出:
192.168.1.11 POST 5200ms
192.168.1.10 GET 3100ms
这里 $5 是第 5 个字段(耗时毫秒),条件 > 3000 满足才会打印。
示例 3:统计与计算——求响应耗时总和与平均值
awk '{sum += $5; count++} END {print "总请求数:", count; print "总耗时:", sum "ms"; printf "平均耗时: %.2f ms\\n", sum/count}' access.log
预期输出:
总请求数: 5
总耗时: 8590ms
平均耗时: 1718.00 ms
这个例子展示了 awk 的 BEGIN/END 块与变量累加能力。END 块在处理完所有行之后执行一次,非常适合做汇总统计。
示例 4:按状态码分组统计(类 SQL 的 GROUP BY)
awk '{code[$4]++} END {for (c in code) print c, code[c]}' access.log | sort -k2 -rn
预期输出:
200 3
500 1
404 1
这里用关联数组 code[$4] 记录每个状态码的出现次数,最后遍历输出。
示例 5:自定义分隔符——处理 CSV 文件
awk -F',' '{print $2, $3}' users.csv
-F',' 指定逗号为字段分隔符,这样就能正确解析 CSV 中的每一列。
4.4 注意事项与常见错误
- $ 的歧义:$1 是字段引用,而 $NF 是“最后一个字段”,不要把 $NF 写成 $NF 之外的自造变量(如 $last)——awk 中变量不带 $,只有字段引用才带。
- 大括号不能省略动作时却忘了写:awk '$5 > 3000' 是合法的,等价于 {print $0};但如果逻辑上需要输出特定字段却没写 print $1,就会整行输出,与预期不符。
- 分隔符与正则:FS 可以设为正则,比如 -F'[,;]' 表示逗号或分号都算分隔符。但要注意转义,复杂场景建议先在小样本上测试。
- 浮点比较:awk 内部使用双精度浮点数,涉及金额等精确计算时可能有精度误差,不适合做金融级精确计算。
- 平台差异:macOS 自带的 BSD awk 与 Linux 的 GNU awk(gawk)在部分函数和选项上有差异,脚本迁移时需验证。
五、知识扩展:正则表达式——三剑客的共同内核
无论是 grep 的匹配、sed 的替换,还是 awk 的字段处理,它们背后都有一个共同的内核:正则表达式(Regular Expression)。理解正则的“编译与匹配”机制,能帮你举一反三。
5.1 两种流派:BRE 与 ERE
POSIX 标准定义了两种正则方言:
- BRE(Basic Regular Expression,基础正则):+、?、|、(、) 等元字符默认是字面量,要表达“元字符含义”需要加反斜杠转义,如 \\+、\\?、\\|、\\(、\\)。
- ERE(Extended Regular Expression,扩展正则):上述元字符默认就是元字符,写出 +、?、| 就能生效,书写更自然。
对应到工具上:
| grep | BRE | grep -E(或 egrep) |
| sed | BRE | sed -E(GNU)/ sed -r |
| awk | ERE | 本身就是扩展正则 |
示例对比:想匹配 ERROR 或 FATAL:
grep "ERROR\\|FATAL" app.log # BRE 写法,| 需转义
grep -E "ERROR|FATAL" app.log # ERE 写法,更直观
5.2 正则引擎的“回溯”与性能
大多数正则引擎采用回溯算法(Backtracking)。当表达式包含大量量词嵌套时,可能触发“灾难性回溯”,导致匹配时间指数级爆炸。一个经典反例:
echo "aaaaaaaaaaaaaaaaaaaaaaaaaaaaab" | grep -E "(a+)+$"
在某些实现上,这条命令会卡住很久。虽然现代 GNU grep 采用了基于 NFA 的 Thompson 算法,避免了灾难性回溯,但在 sed、awk 或其他语言中仍可能遇到。因此编写正则时要尽量避免不必要的嵌套量词。
5.3 贪婪与懒惰匹配
默认情况下,*、+ 都是贪婪匹配——尽可能多地吃字符。想改为懒惰匹配,需要加 ?:
贪婪:<.*> 会匹配 "<a> <b>" 整段
懒惰:<.*?> 只匹配 "<a>" 部分
不过要注意:sed 和 grep 的正则并不支持懒惰量词(.*? 中的 ? 会被当作普通字符或语法错误),这是很多从其他语言转过来的同学容易踩的坑。awk 同样不支持。需要“最短匹配”时,往往要改写正则,比如用排除字符类 [^>]* 代替 .*?:
grep -oE "<[^>]*>" test.html # 匹配最短的 <…> 标签
六、三剑客组合实战
三剑客单独使用已经很强,组合起来才是真正的生产力。以下给出几个贴近真实工作的组合案例。
6.1 查日志:快速定位错误上下文
grep -n -E "ERROR|FATAL" app.log | sed -n '1,20p'
先把所有错误行带行号抓出来,再用 sed 只看前 20 条,避免刷屏。
6.2 统计访问量 Top 5 的 IP(awk + sort + head 联用)
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -5
awk 先抽取每行第一个字段(IP),再交给 sort | uniq -c 统计,最后排序取前五。更进阶的写法是纯 awk 数组,但管道组合更易读。
6.3 批量修改多台机器配置(sed 脚本化)
把要执行的替换写进 change.sed 文件:
# 注释掉旧的端口号
s/^Port 22/Port 2222/
# 删除空行
/^$/d
# 统一 False 为 false(忽略大小写)
s/False/false/gI
然后批量执行:
for host in server{01..20}; do
scp change.sed "$host":/tmp/
ssh "$host" "sed -i.bak -f /tmp/change.sed /etc/ssh/sshd_config"
done
6.4 从 Nginx 日志生成 Markdown 报表(awk 综合应用)
awk '
{
status[$4]++
if ($5 > 3000) slow++
total++
}
END {
print "| 状态码 | 数量 |"
print "|——–|——|"
for (s in status) printf "| %s | %d |\\n", s, status[s]
print ""
printf "- 总请求数:%d\\n", total
printf "- 慢请求数(>3s):%d\\n", slow
}' access.log
预期输出(示例):
| 状态码 | 数量 |
|——–|——|
| 200 | 3 |
| 500 | 1 |
| 404 | 1 |
– 总请求数:5
– 慢请求数(>3s):2
七、最佳实践
八、常见错误汇总
| 模式/命令不加引号 | 空格或 * 被 shell 提前展开,匹配结果错误 | 用单引号包裹模式和脚本 |
| sed -i 前不备份 | 替换错误后文件无法恢复 | 使用 -i.bak 或先 cp |
| grep -r 不限定路径 | 扫描范围大、耗时长 | 限定目录,或先 find 过滤 |
| awk 中用 $变量名 取字段 | 理解错误,输出异常 | 记住字段固定用 $1、$2、$NF |
| 混淆 > 与 >> | 误覆盖重要文件 | 追加用 >>,覆盖需再三确认 |
| 把 -n 和 p 拆开用 | 输出重复或漏行 | sed -n '…p' 成对使用 |
九、总结与练习
9.1 小结
本文从三剑客的分工讲起,依次深入了 grep 的搜索过滤、sed 的流式编辑、awk 的字段分析,并补充了正则表达式底层机制、组合实战、最佳实践与常见错误。核心要点回顾:
- grep:负责“找”,用 -i -v -n -c -E -o -C 等选项灵活过滤。
- sed:负责“改”,用 s///、d、p、a/i 等命令逐行编辑,-i 需慎用。
- awk:负责“算”,用 $1、NF、NR、BEGIN/END 和关联数组完成分组统计与格式化输出。
- 三者都基于正则,理解 BRE/ERE 的差异是进阶的关键。
9.2 练习题
请动手完成以下练习,巩固今天的内容。
练习 1:日志过滤 从一个 Web 访问日志中,找出所有“状态码为 500 并且请求方法为 POST”的行,并显示行号。
提示:可组合 grep -n 与正则,或用 awk 做多条件判断。
练习 2:配置清洗 编写一条 sed 命令,删除某配置文件中的所有空行和以 # 开头的注释行,并直接在原文件上修改(记得备份)。
提示:sed -i.bak 配合多个 -e 命令。
练习 3:统计 IP 用一条命令统计出访问日志中出现次数最多的前 3 个 IP。
提示:awk '{print $1}' | sort | uniq -c | sort -rn | head -3。
练习 4:报表生成 给定若干行学生成绩数据,每行格式为“姓名 语文 数学 英语”,请用 awk 计算每名学生的总分与平均分,并在最后输出全班的“语文平均分”。
提示:用数组累加每个学生总分,用独立的 sum 变量统计全班语文总分,在 END 块输出。
练习 5:正则进阶 构造一条正则,从一个 HTML 文件中提取所有 href 属性里的 URL 地址,要求使用懒惰匹配或排除字符类实现最短匹配。
提示:grep -oE 'href="[^"]*"',再配合 sed 去掉 href=" 前缀和末尾引号。
写在最后:三剑客的价值不在记忆参数,而在“遇到问题先想用什么工具、怎么组合”的思维。建议把这篇文章收藏起来,找一台 Linux 机器或 WSL,把每个示例亲手敲一遍——当你第一次用一行管道从百万行日志中精准捞出问题的那一秒,你会真正理解什么叫“山高路远,值得修炼”。




