文本处理三剑客的实战拆解:grep、awk、sed 到底该怎么用
很多人第一次接触命令行文本处理,都是从一条grep开始的。查日志、筛配置、找报错,一条命令下去,屏幕上刷刷刷滚出结果,那种感觉确实很爽。但真到了要批量改文件、做字段统计、按条件重组数据的时候,光靠grep就不够用了,于是开始翻awk和sed的手册,结果被一堆$1、NR、s///g搞得头晕。我见过不少同行,这三个命令用了好几年,实际只会grep 关键字 文件和sed -i 's/旧/新/g',稍微复杂一点的需求就退回写脚本,效率白白丢掉一大截。
这篇内容就是想把grep、awk、sed这三个命令从“会用”拉到“用透”的层面。我不打算照着手册逐条翻译参数,而是按真实工作里最常见的场景来拆:怎么精准筛选、怎么按字段做统计、怎么安全地批量替换。每个命令都会讲清楚它解决什么问题、核心机制是什么、参数为什么这么选,再配上可以直接复制运行的实例。不管你是刚接触命令行的新手,还是已经能写点 shell 但总觉得不够利索的老手,应该都能从里面抠出几招能马上用上的东西。
1. 先把三个命令的分工掰清楚
在动手之前,有个认知必须先建立起来:grep、awk、sed不是三个可以互相替代的工具,它们各自负责文本处理流水线上的不同环节。搞混分工,是很多人用起来别扭的根源。
1.1 三者的定位差异:找、改、算
用一句话概括:grep负责找,sed负责改,awk负责算。
grep的本质是一个行过滤器。它逐行读入文本,判断每一行是否匹配给定模式,匹配就输出,不匹配就丢弃。它不关心行里面的结构,只关心“这一行整体符不符合条件”。所以它最擅长的事情是:从一大堆内容里捞出包含某个特征的行。
sed的本质是一个流编辑器。它也是逐行处理,但它不只是判断,而是可以对匹配到的内容做替换、删除、插入、追加等编辑动作。关键在于“流”字——它不需要把整个文件读进内存再改,而是一边读一边改一边输出,所以处理大文件时内存占用很稳。
awk的本质是一个字段级处理语言。它把每一行按分隔符切成若干字段,你可以按字段做条件判断、数值计算、格式化输出,还支持变量、数组、循环、函数。严格来说它已经是一门小型编程语言了,只是语法紧凑到可以写成一行。
理解了这个分工,很多选择就自然清楚了:只是筛行,用grep;要改内容,用sed;要按列统计或做复杂条件处理,用awk。当然实际工作中三者经常串起来用,管道一接,各干各的活。
1.2 为什么很多人只学会了 grep
这里插一句观察。大部分人卡在grep阶段,不是因为awk和sed难,而是因为没有遇到非用不可的场景。日常的“找一下”需求,grep确实够了。真正逼着人往上走的,往往是这几类任务:日志里按状态码统计次数、配置文件批量改某个字段、从结构化文本里抽某几列重排。这些任务用grep硬做会很别扭,用awk或sed就是几秒钟的事。
所以下面的内容,我会尽量从“什么场景下必须用它”切入,而不是干讲语法。语法查手册就有,场景判断才是经验。
2. grep:不只是“搜关键字”
grep看起来最简单,但它的参数组合能玩出的花样比想象中多。很多人只用过grep 关键词 文件,其实它的正则能力、上下文控制、文件级筛选都相当能打。
2.1 基础匹配与正则的取舍
最基本的用法不用多说:
grep "error" app.log这会在app.log里找出所有包含error的行。但这里有个容易被忽略的点:默认情况下grep用的是基本正则表达式(BRE),某些元字符需要转义才能生效。比如你想匹配“以 error 开头”的行,写grep "^error"没问题,但想用+、?、|这些扩展元字符,就得加-E切到扩展正则(ERE),或者老老实实加反斜杠。
我的建议是:只要用到稍微复杂一点的正则,直接上-E。可读性好,不容易因为转义漏写而匹配错。对比一下:
# BRE 写法,竖线要转义 grep "error\|warning" app.log # ERE 写法,清爽 grep -E "error|warning" app.log两种都能跑,但后者明显更好维护。团队协作时,别人读你的命令不用在脑子里做转义换算。
还有一个高频需求是忽略大小写,加-i即可。日志里Error、ERROR、error混着出现是常态,不加-i很容易漏。
grep -iE "error|fail|exception" app.log这条命令在实际排查问题时出场率极高,一次把三类异常关键词都捞出来。
2.2 上下文参数:-A、-B、-C 的实战价值
只输出匹配行,很多时候信息不够。比如日志里报了一行NullPointerException,你光看这一行根本不知道前面发生了什么。这时候上下文参数就派上用场了:
-A n:输出匹配行之后的 n 行(After)-B n:输出匹配行之前的 n 行(Before)-C n:输出匹配行前后各n 行(Context)
grep -B 3 -A 5 "NullPointerException" app.log这条命令会把异常行前面 3 行、后面 5 行一起打出来,通常足够看清调用栈的开头部分。我个人的习惯是排查异常时用-B 5 -A 10,因为异常信息往往在抛出点之后才展开,前面几行则是触发条件。
提示:
-A、-B、-C输出的上下文行之间会用--分隔,多个匹配块不会糊在一起,这个细节在结果很长时很有用。
2.3 递归搜索与文件类型过滤
在项目目录里找某个函数在哪定义,是日常高频操作。-r递归搜索配合--include限定文件类型,能避免把二进制文件、日志、依赖目录全扫一遍:
grep -rn --include="*.py" "def process_data" ./src这里-n是显示行号,定位问题时必不可少。--include支持通配符,也可以写多个。反过来还有--exclude和--exclude-dir,用来排除node_modules、.git这类不该搜的目录:
grep -rn --exclude-dir={.git,node_modules,dist} "TODO" .不加排除的话,在大项目里搜一次可能要等十几秒,加上之后通常一秒内出结果。这个差别在反复搜索时非常明显。
2.4 反向匹配与计数:-v 和 -c 的组合拳
-v是反向匹配,输出不包含模式的行。这个参数在过滤噪音时特别好用。比如看配置文件时想跳过所有注释行和空行:
grep -vE "^\s*#|^\s*$" config.conf^\s*#匹配以可选空白开头后跟#的行,^\s*$匹配空行,-v把它们都排除掉,剩下的就是有效配置。
-c是只输出匹配的行数,不输出内容。它和-v组合能快速统计有效行数:
grep -vcE "^\s*#|^\s*$" config.conf再进一步,-o配合sort | uniq -c可以做词频统计。-o的意思是只输出匹配到的部分,而不是整行。比如统计日志里各状态码出现次数:
grep -oE "status=[0-9]{3}" access.log | sort | uniq -c | sort -rn这条管道先抽出所有status=xxx片段,排序后去重计数,再按次数倒序排列。虽然严格说统计活是awk更擅长,但grep -o这条路在临时排查时写起来更快。
2.5 grep 的边界:什么时候该换工具
grep的短板很明确:它不理解字段结构。一旦需求变成“第二列大于 100 的行”或者“按第三列分组求和”,grep就无能为力了,硬做只能靠一堆正则拼凑,既脆弱又难读。这种时候别硬扛,直接换awk。同样,需要修改内容时grep只能看不能改,得交给sed。
3. sed:流式编辑的稳与险
sed最出名的用法是替换,但它能做的远不止替换。删除行、插入行、按行号操作、多点编辑,都是它的常规能力。不过sed也是三个命令里最容易出事的一个,尤其是-i原地修改,用不好会把文件改坏。
3.1 替换命令 s 的完整语法拆解
替换的基本形式是s/模式/替换内容/标志。很多人只知道s/旧/新/g,其实标志位和分隔符都有讲究。
先说分隔符。默认用/,但如果模式或替换内容里本身含/(比如处理路径),就得转义,很烦。这时候可以换分隔符,sed允许用任意字符,常见的是#或|:
# 处理路径时用 # 当分隔符,避免转义 sed 's#/old/path#/new/path#g' config.txt再说标志位:
| 标志 | 含义 | 使用场景 |
|---|---|---|
g | 全局替换,一行内所有匹配都换 | 一行可能出现多次目标时 |
| 无 | 只替换每行第一个匹配 | 明确只想改第一处时 |
p | 打印匹配行(常配-n) | 只想看被替换的行 |
i | 忽略大小写(GNU sed 支持) | 大小写混杂时 |
| 数字 n | 只替换第 n 个匹配 | 精确定位某一处 |
g是最容易漏的。不加g时,sed只替换每行的第一处匹配,如果一行里有多个目标,后面的会被漏掉。这个坑我见过太多次——有人改完配置发现只改了一半,排查半天才发现是漏了g。
3.2 原地修改 -i 的正确姿势与备份策略
-i是原地修改,直接把结果写回文件。方便是真方便,危险也是真危险——改错了没有撤销。所以我的习惯是永远带备份后缀:
sed -i.bak 's/old_value/new_value/g' config.conf这样原文件会被保存成config.conf.bak,改坏了随时能恢复。等确认没问题再删备份。多花一秒钟,省掉可能的半小时。
还有一个跨平台差异要注意:某些系统的sed -i要求后面必须跟一个参数(哪怕是空字符串),写法是sed -i '' 's/.../.../',而 GNU 版本则直接sed -i 's/.../.../'。如果你写的脚本要在不同环境跑,这一点会导致命令直接报错。稳妥的做法是先确认目标环境的sed版本,或者在脚本里做兼容判断。
注意:
-i修改前务必确认模式匹配范围。一个写得太宽的正则可能匹配到意料之外的行,改完才发现问题就晚了。建议先不加-i跑一遍看输出,确认无误再加。
3.3 按行号与模式删除、插入、追加
sed的删除命令是d,可以按行号删,也可以按模式删:
# 删除第 5 行 sed '5d' file.txt # 删除第 3 到第 8 行 sed '3,8d' file.txt # 删除所有空行 sed '/^\s*$/d' file.txt # 删除包含 DEBUG 的行 sed '/DEBUG/d' file.txt插入用i(在匹配行前插入),追加用a(在匹配行后追加):
# 在每个 [section] 前插入一行注释 sed '/^\[.*\]$/i # ---- section start ----' config.ini # 在文件末尾追加一行 sed '$a # end of file' config.ini这里$表示最后一行。i和a后面跟的内容如果含特殊字符,建议用单引号包住整个脚本,避免 shell 提前解释。
3.4 多点编辑与脚本化:-e 和 -f
当需要一次做多个编辑动作时,用-e串联:
sed -e 's/foo/bar/g' -e 's/baz/qux/g' -e '/^#/d' file.txt多个-e按顺序执行,前一个的结果会作为后一个的输入。如果编辑逻辑很长,写成脚本文件更清晰,用-f引用:
# 把编辑规则写进 rules.sed # s/foo/bar/g # s/baz/qux/g # /^#/d sed -f rules.sed file.txt脚本化还有个好处:规则可以版本管理,团队共享,比每次手敲一长串命令可靠得多。
3.5 sed 处理大文件时的内存表现
前面提过sed是流式处理,逐行读逐行输出,所以即使文件有几个 G,内存占用也基本恒定。这一点和“先读进内存再处理”的方式有本质区别。实际测试中,处理一个几百万行的日志文件,sed的内存占用通常稳定在几 MB 级别,速度也很快。
但要注意,-i原地修改时,sed实际上是写一个临时文件再替换原文件,所以磁盘上需要额外空间容纳新文件。如果磁盘快满了,-i可能中途失败。这种时候可以先输出到新文件,确认后再手动替换。
4. awk:字段级处理的真正威力
如果说grep和sed是“行”级别的工具,那awk就是“字段”级别的。它把每行拆成列,让你能按列做判断、计算、格式化,这是它区别于前两者的核心能力。很多人觉得awk语法怪,其实它的结构非常规整:模式 { 动作 },模式决定哪些行参与处理,动作决定对这些行做什么。
4.1 内置变量:$0、$1、NR、NF、FS、OFS
awk的内置变量是理解它的钥匙,先把这几个吃透,大部分用法就通了:
| 变量 | 含义 | 典型用途 |
|---|---|---|
$0 | 整行内容 | 输出原始行 |
$1~$n | 第 n 个字段 | 按列取值 |
NR | 当前行号(跨文件累计) | 加行号、按行筛选 |
NF | 当前行的字段数 | 取最后一列、判断列数 |
FS | 输入字段分隔符 | 指定按什么切列 |
OFS | 输出字段分隔符 | 控制输出格式 |
举个最直观的例子,打印文件的第二列和第四列:
awk '{print $2, $4}' data.txt默认按空白(空格或制表符)切分。如果数据是逗号分隔的 CSV,就要指定FS:
awk -F',' '{print $1, $3}' data.csv-F是FS的命令行快捷写法。也可以用-F '\t'处理制表符分隔的文件。
NF有个经典技巧:取每行最后一列。因为字段数不固定,不能写死$5,但$NF永远指向最后一个字段:
awk '{print $NF}' data.txt倒数第二列则是$(NF-1)。这个写法在处理列数不定的日志时特别顺手。
4.2 模式匹配与条件过滤
awk的模式部分可以省略(表示所有行),也可以是条件表达式。比如只处理第三列大于 100 的行:
awk -F',' '$3 > 100 {print $1, $3}' data.csv注意这里$3 > 100是数值比较,awk会自动把字段转成数字。如果要字符串比较,用==或~(正则匹配):
# 第五列包含 error 的行 awk -F',' '$5 ~ /error/ {print $0}' data.csv # 第五列不包含 error 的行 awk -F',' '$5 !~ /error/ {print $0}' data.csv~是正则匹配,!~是反向。这个能力让awk在筛选时比grep更精准,因为可以限定“只在某一列里找”,而不是整行找。比如日志里error可能出现在消息正文里,但你只想找状态列是 error 的行,用awk就能精确锁定。
4.3 统计求和、计数、求平均的写法
awk做统计是它的看家本领。求和:
awk -F',' '{sum += $3} END {print sum}' data.csv计数:
awk 'END {print NR}' data.txtNR在处理到END时就是总行数,所以这条命令等价于wc -l,但awk还能顺便做别的。
求平均:
awk -F',' '{sum += $3; count++} END {print sum/count}' data.csv按分组统计是更常见的需求。比如按第一列分组,对第三列求和:
awk -F',' '{sum[$1] += $3} END {for (k in sum) print k, sum[k]}' data.csv这里sum是一个关联数组,键是分组名,值是累加和。END块里遍历数组输出。这个模式几乎可以套用到所有“按某列分组做聚合”的场景,改一下累加逻辑就行。
再进一步,按分组求平均并保留两位小数:
awk -F',' '{sum[$1] += $3; cnt[$1]++} END {for (k in sum) printf "%s %.2f\n", k, sum[k]/cnt[k]}' data.csvprintf的格式控制和 C 语言一致,%.2f保留两位小数。输出对齐、格式统一时用它比print更可控。
4.4 BEGIN 与 END 块的使用时机
awk的三个块:BEGIN、主体、END。BEGIN在读取任何行之前执行一次,适合初始化变量、打印表头;END在所有行处理完后执行一次,适合输出汇总结果。
awk -F',' 'BEGIN {print "name,total"} {sum[$1] += $3} END {for (k in sum) print k "," sum[k]}' data.csv这条命令先打印表头,再输出分组汇总,结果直接就是一份可读的报表。BEGIN里还可以设置FS、OFS,比命令行参数更灵活:
awk 'BEGIN {FS=","; OFS="\t"} {print $1, $3}' data.csv这样输入按逗号切,输出用制表符分隔,适合后续粘贴到表格软件里。
4.5 awk 与 grep、sed 的管道协作
三者串起来用才是完整形态。举个实际场景:从日志里筛出错误行,提取时间戳和错误码,再按错误码统计。
grep -E "ERROR" app.log \ | awk '{print $1, $5}' \ | awk '{count[$2]++} END {for (c in count) print c, count[c]}' \ | sort -k2 -rn第一步grep筛出错误行,第二步awk抽时间戳和错误码两列,第三步按错误码计数,最后排序。每一步职责单一,组合起来却完成了相当复杂的分析。这种“小工具串管道”的思路,比写一个大而全的脚本更容易调试——哪一步结果不对,单独跑那一步就能定位。
5. 三个命令的选型判断与组合套路
学到这里,语法层面基本齐了,但实际用的时候还是会犹豫“这个需求该用哪个”。这一节把常见的判断逻辑和组合模式整理出来,相当于一份速查。
5.1 一张表看懂该用谁
| 需求 | 首选 | 理由 |
|---|---|---|
| 找包含某关键词的行 | grep | 行过滤是它的本职 |
| 找某列满足条件的行 | awk | 能定位到字段,比整行匹配精准 |
| 批量替换文本 | sed | 流式替换,大文件也稳 |
| 按列统计求和/平均 | awk | 字段级计算是它的强项 |
| 删除/插入特定行 | sed | 行级编辑直接支持 |
| 多条件组合筛选 | awk | 支持逻辑运算和正则 |
| 递归搜目录 | grep -r | 递归和文件过滤现成 |
| 格式化输出报表 | awk | printf控制格式 |
判断的核心就一句话:看需求落在“行”还是“字段”上。行级筛选和编辑交给grep/sed,字段级计算和条件交给awk。
5.2 常见组合模式:筛选-转换-统计
最经典的三段式管道:grep筛、sed清洗、awk统计。
grep "2024-" app.log \ | sed 's/\[//g; s/\]//g' \ | awk '{print $2, $6}' \ | awk '{sum[$1] += $2} END {for (k in sum) print k, sum[k]}'这个模式的好处是每一步都能单独验证。先跑grep看筛出来的行对不对,再加sed看清洗结果,最后加awk看统计。出问题时不用猜是哪一步的锅。
另一个常见组合是awk生成sed脚本,做条件替换。比如只替换满足某条件的行:
awk -F',' '$3 > 100 {print "s/^" $1 "/REPLACED/"}' data.csv > fix.sed sed -f fix.sed target.txt先用awk根据数据生成替换规则,再交给sed执行。这种“数据驱动编辑”的思路在批量处理时很有用,规则不是写死的,而是从数据里算出来的。
5.3 性能对比:什么时候别硬用管道
管道虽好,但不是所有场景都适合。三个命令串起来意味着数据要经过多次读写,如果数据量特别大(比如上千万行),管道开销会累积。这种时候可以考虑用单个awk完成全部逻辑,因为awk本身就能做筛选、转换、统计:
awk -F',' '$3 > 100 {sum[$1] += $3} END {for (k in sum) print k, sum[k]}' data.csv一条awk顶替了grep | sed | awk三段,少了两轮数据传递。当然,可读性会下降一些,所以我的原则是:数据量小、逻辑清晰时用管道;数据量大、性能敏感时合并到单个awk。这个取舍没有绝对标准,跑一下time对比最直接。
6. 踩过的坑与实战心得
语法和套路讲完,最后这部分是我在实际使用中攒下来的经验,有些是踩坑换来的,有些是反复用之后总结的顺手写法。这些内容手册里不会写,但真正影响效率。
6.1 正则转义与特殊字符的连环坑
grep、sed、awk三者的正则方言并不完全一致,这是最容易出错的地方。grep默认 BRE,sed默认也是 BRE,awk则更接近 ERE。同一个正则在不同命令里可能表现不同。
举个具体的:+在 BRE 里是普通字符,要匹配“一个或多个”得写\+;在 ERE 里+才是量词。所以grep "a+"匹配的是字面的a+,而grep -E "a+"匹配一个或多个a。这个差异如果不注意,匹配结果会完全不对。
另一个高频坑是sed替换内容里的特殊字符。替换内容中&表示“整个匹配”,\1表示第一个捕获组。如果你想替换成字面的&,得写成\&。比如把A&B替换成A&B本身,直接写s/A&B/A&B/会因为&被解释而得到奇怪结果,正确写法是s/A&B/A\&B/。
提示:拿不准正则行为时,先用
echo "测试字符串" | grep/sed/awk ...小范围验证,确认匹配范围再上真实文件。这个习惯能挡掉大部分误操作。
6.2 大文件处理时的内存与速度权衡
处理大文件时,三个命令的表现差异明显。grep和sed都是流式的,内存占用稳定;awk如果用了关联数组做分组统计,内存会随分组数增长。分组数少(比如按状态码分组,就几十个键)没问题,但如果按用户 ID 分组、用户有几百万,数组就会吃掉大量内存。
这种时候的应对办法是:要么先sort再顺序处理(牺牲时间换内存),要么分批处理。我遇到过一次按请求 ID 统计的场景,直接awk数组把内存撑爆了,后来改成先sort再逐段处理才稳住。所以看到“按高基数字段分组”的需求,心里要有个预警。
速度方面,grep通常最快,因为它只做匹配不做别的;sed次之;awk因为功能多,单行处理开销略大,但胜在一次能做完所有事,综合下来未必慢。具体场景跑time最靠谱,别凭感觉。
6.3 先预览再落盘:我的操作习惯
这条是我最想强调的:任何带-i或重定向到原文件的命令,执行前先不加修改地跑一遍看输出。
# 第一步:预览,确认匹配范围 sed 's/old/new/g' config.conf | grep -n "new" # 第二步:确认无误再原地改,且带备份 sed -i.bak 's/old/new/g' config.conf多花这十几秒,能避免绝大多数“改错了文件”的事故。尤其是正则写得比较宽的时候,预览能直接暴露“匹配到了不该匹配的行”这个问题。我见过有人一个s/.*/.../下去把整个文件改成一行的,就是因为没预览。
6.4 把常用组合固化成小脚本
反复用的命令组合,值得固化成脚本或别名。比如我常用的“统计日志里各错误码次数”:
#!/bin/bash # errstat.sh - 统计日志中各错误码出现次数 LOG_FILE="${1:?请传入日志文件路径}" grep -E "ERROR|FATAL" "$LOG_FILE" \ | awk '{print $5}' \ | sort | uniq -c | sort -rn存成errstat.sh,加执行权限,以后./errstat.sh app.log一条命令出结果。比每次手敲管道快得多,也不容易敲错。类似的还有“清理配置文件注释行”“按列重排 CSV”等,用顺手了就固化下来,日积月累能省下大量重复劳动。
我个人在实际操作中的体会是,这三个命令的价值不在于语法多复杂,而在于把判断和操作拆得足够细。grep只管找,sed只管改,awk只管算,每个工具做一件事并做好,再用管道串起来。真正熟练之后,面对一个文本处理需求,脑子里第一反应不是“写个脚本”,而是“这条管道该怎么接”。这个思维转变,比记住多少参数都重要。