欢迎光临
我们一直在努力

Shell 文本处理三剑客:grep sed awk 从入门到实战

/* 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 处理每一行时:

  • 先按分隔符(默认空白字符)把行切成 $1、$2、$3…… $0 是整行。
  • 判断“模式”是否满足,满足则执行大括号中的“动作”。
  • 模式或动作都可省略:省略模式表示每行都执行;省略动作(只写模式)等价于 {print $0}。
  • 内置变量速查:

    变量含义
    $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,扩展正则):上述元字符默认就是元字符,写出 +、?、| 就能生效,书写更自然。

    对应到工具上:

    工具默认正则切换到 ERE
    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


    七、最佳实践

  • 先备份,再 -i:用 sed -i 修改文件前,先以 sed -i.bak 形式生成备份,或手动 cp 一份,防患未然。
  • 小样本先行:正则和命令先在 head -50 file > /tmp/sample 的小样本上验证,确认无误后再全量执行。
  • 善用管道逐步调试:不要一上来就写一条超长管道。逐段执行,确认每段输出符合预期后再拼接,排查效率极大幅提升。
  • 给复杂脚本加注释:awk 和 sed 的脚本一旦超过 5 行,建议写成独立文件(-f script.awk / -f script.sed),并用 # 注释说明意图,方便日后维护与交接。
  • 优先用工具的本职能力:要“查”用 grep,要“改”用 sed,要“算”用 awk。用 awk 做简单替换虽然可以,但可读性差,不是好实践。
  • 别在生产环境“裸奔”:大文件上直接跑复杂正则可能吃满 CPU 或内存。优先使用 grep -F(固定字符串,无正则开销)处理纯关键词搜索;对大文件,先考虑 LC_ALL=C 环境下运行,可显著提升 grep 性能。

  • 八、常见错误汇总

    常见错误后果正确做法
    模式/命令不加引号 空格或 * 被 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,把每个示例亲手敲一遍——当你第一次用一行管道从百万行日志中精准捞出问题的那一秒,你会真正理解什么叫“山高路远,值得修炼”。

    赞(0)
    未经允许不得转载:171主机测评 » Shell 文本处理三剑客:grep sed awk 从入门到实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址