ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux文本处理三板斧:grep、sed、awk实战指南

Linux文本处理三板斧:grep、sed、awk实战指南

最近带一位刚接触命令行的朋友排查日志,他盯着一个几百MB的文件发愁:“用编辑器打开都要卡半天,怎么才能快速找到那一行报错?”我说,你需要的不是打开它,而是“找”“切”“汇总”三步,对应的正是Linux里最经典的三板斧:grep、sed、awk。再配合管道符把数据在命令之间流转,本来要人工作几小时的事情,一两行命令就能搞定。这篇内容就是给所有刚入门Linux、想系统掌握文本处理思路的人准备的,不需要提前背多少参数,跟着场景走一遍,你就能理解为什么说“Linux下一切皆文件”,以及如何用这三个命令对文本做真正的加工。

很多人以为这三个命令是三个孤立的小工具,其实它们更像一条流水线:grep负责“从一堆里找出我想要的”,sed负责“按行把内容收拾干净”,awk负责“把每行拆成字段算账”。一旦你理解了它们各自的定位,再看到复杂命令时,就不会觉得是天书。我尽量从设计思路讲起,把参数放在真实场景里解释,最后再给出一份常见问题速查,方便你以后踩坑时回来翻。

1. 三板斧是什么,为什么绕不开它们

1.1 从“Linux一切皆文件”说起

Linux系统里,配置文件是文本,运行日志是文本,命令的输出也是文本。哪怕一个程序内部是二进制数据,对外提供的往往也是文本格式的接口。文本虽然没有数据库那么结构化,但它几乎无处不在,而且处理起来足够灵活。文本处理三板斧——grep、sed、awk,就是围绕这种“行式文本”设计的三个基础工具。

grep的定位是“查找”,它像扫描仪一样逐行读入内容,把匹配某个模式的行挑出来;sed的定位是“过滤与编辑”,它能在不打开文件的情况下直接删除、替换、插入指定行;awk的定位更偏“数据计算”,它会自动把每一行按分隔符拆成多个字段,然后允许你对字段做判断、统计、格式化输出。三者之间不是竞争关系,而是互相配合的关系。

1.2 三者的边界与协作

我见过不少新手一开始就纠结“我到底该学grep还是awk”。其实它们分工很清晰,用一句话概括:grep负责按模式找行,sed负责对行做增删改,awk负责按字段做计算。如果把一行文本看成“一条记录”,grep是筛选记录,sed是编辑记录,awk是对记录的各个属性做运算。

你可以很快把它们的核心能力放进一张表里,先记住边界,后面用起来就不会含糊:

命令擅长的操作典型场景最常用用法
grep按正则匹配并输出某行从日志中找报错grep "ERROR" app.log
sed按行地址或模式进行替换/删除/插入批量改配置文件、清理文本sed 's/old/new/g'
awk按分隔符拆字段,做统计/条件判断求平均值、统计IP次数、格式化打印awk '{print $1, $NF}'

从处理粒度上也可以这样理解:grep只关心行,sed也只关心行(但可以修改行),awk关心的是行里的“列”。所以当你发现一个任务需要“先找出特定的行,再把行里的某些字段拿出来算一算”时,基本就是grep与awk的组合。而sed常常用来做中间一步的清洗,比如去掉行首的时间戳、把某个分隔符统一替换掉,让awk的后续拆分更稳定。

1.3 理解标准流:数据流转的地基

三板斧能串联起来,真正依靠的是Linux的“标准流”机制。每个命令启动后,都会从“标准输入(stdin)”读取数据,把结果写到“标准输出(stdout)”,把错误信息写到“标准错误(stderr)”。文件也可以被当作输入,但更高级的用法是让一个命令的标准输出直接变成下一个命令的标准输入,这就是管道符|做的事情。

管道和重定向是数据流转的两种基本方式。管道侧重“命令与命令之间的传递”,重定向侧重“把数据送到文件或从文件取回来”。比如grep ERROR app.log | wc -l,就是让grep输出的行不断增加地流到wc里统计行数;而grep ERROR app.log > error.log则是把结果保存到文件里。理解了“数据像水一样流过命令”之后,再长的命令链也能一步步拆开来看。

2. 查找:grep,从大海捞针到精准定位

2.1 grep 的基本参数:先把最常用的记下来

grep的全称是“global regular expression print”,它默认只做一件事:按正则表达式匹配行,并把匹配的行打印到标准输出。基础语法是grep [选项] "模式" 文件。如果文件省略,grep会从标准输入读取,这正是它常用在管道开头的原因。

我最常用的几个参数,按实用程度排序:

  • -E:启用扩展正则,省去大量反斜杠转义。比如grep -E "error|fail" app.log可以匹配两种内容。
  • -i:忽略大小写。排查日志时很有用,因为有的程序会写成ERROR,有的会写成error。
  • -v:反向匹配,即“排除掉包含模式的行”。比如grep -v "^#" config.conf可以忽略注释行。
  • -n:显示匹配行在文件中的行号,方便回头定位。
  • -c:只统计匹配行数量,而不是打印内容。
  • -l:只显示“包含匹配内容的文件名”,常用于在多个文件里找哪个文件出问题。
  • -r或-R:递归搜索目录下的所有文件。
  • -w:匹配整个单词,避免把“error”匹配到“error_handler”这种词里。
  • -o:只输出匹配到的部分,而不是整行,在提取特定内容时非常顺手。

这些参数可以组合使用。打个比方:grep -rniE "timeout|retry" /var/log/就是在/var/log/目录下递归查找,忽略大小写,只要匹配timeout或retry,并且显示行号。这种写法在故障排查时很常用。

2.2 正则表达式入门:不需要背,但要会查

grep的威力一半来自正则表达式。正则就是一套描述字符串模式的语言,核心元字符并不多。基础正则里,^匹配行首,$匹配行尾,.匹配任意一个字符,*表示前一个字符重复0次或多次,[]表示字符集合,[^]表示不在集合中。比如^2024-能匹配以“2024-”开头的行,error$能匹配以error结尾的行。

启用-E后,常见的扩展写法还有:+表示前一个字符出现至少1次,?表示出现0次或1次,|表示或,()用于分组,{m,n}表示出现次数范围。我自己常用的一个例子是匹配IP地址:grep -E "^[0-9]{1,3}(\.[0-9]{1,3}){3}" access.log。虽然这个表达式无法严格校验IP是否合法(比如“999.1.1.1”也会被匹配到),但用来从日志中快速抓IP已经很够用。

这里给新手一个学习建议:不要试图一天内把所有正则语法都背下来,先记住^ $ . * [ ]这五个,在用的时候慢慢拓展到+ ? | () {}。正则的难点不是语法,而是“抽象地描述你要找的内容”,这个能力只能靠多看例子、多写几次来培养。

2.3 实战:从日志里精准抓取错误现场

假设有一个app.log,内容混着INFO、WARN、ERROR三种级别,每条日志开头是时间戳。我想快速知道今天有哪些ERROR,并且大概什么时候发生的,可以这样:

# 查看所有ERROR级别日志,显示行号并忽略大小写 grep -ni "error" app.log # 只看ERROR后的堆栈第一行,排除掉常见的空行和"at com.xxx" grep -A1 "ERROR" app.log | grep -v "^--$" | grep -v "at " | head -50

-A1表示输出匹配行的后1行,这在看异常堆栈时很有用;-B1则输出前1行,用于看上下文。如果你只是想统计某个错误出现了多少次,用grep -c "特定错误" app.log即可。但要注意,-c统计的是“匹配行数量”,不是“出现次数”,如果一行里出现了两次关键字,它也只算一次。想统计准确的出现次数,可以用grep -o "关键字" app.log | wc -l。

我踩过一个很典型的坑:想从代码里找出哪些引用了一个旧函数,结果因为忘了加-w,把所有包含类似字段名的行都搜出来了,多了几十倍假结果。所以当你想精确匹配某个“完整词”时,别吝啬-w。想进一步排除干扰内容时,再叠加-v,比如grep -w "createUser" *.php | grep -v "function createUser",就能找出调用点而不是定义点。

3. 过滤:sed,流式处理与批量修改

3.1 sed 的工作模型:每一次处理都是逐行过

sed是“流编辑器”,它的设计思想非常朴素:一次读一行,把这一行放进“模式空间”,执行你给出的编辑命令,然后输出结果。整个过程不会把整个文件一次性加载到内存,所以即使处理几个GB的日志,它也能从容应对。

sed的语法结构是sed [选项] '脚本' 文件。脚本里最常写的两部分是“地址范围”和“操作命令”。地址范围指定对哪些行生效,操作命令指定做什么。例如sed -n '10,20p' file,-n关闭默认输出,10,20p表示只打印第10到第20行。没有地址时,命令作用于所有行,比如sed 's/foo/bar/' file会替换所有行里的第一个foo。

理解地址范围是sed的关键。地址可以是一个行号(5)、一个区间(2,8)、一个模式(/pattern/),也可以组合(/start/,/end/)。这些看似简单,实际能玩出很多花样。比如只修改某个配置块内的旧参数,就可以写sed '/[mysqld]/,/^$/ s/port=3306/port=3307/' my.cnf,它只会在[mysqld]到第一个空行之间替换端口配置。

3.2 高频指令:增删改查一次看完

sed的指令不算多,日常高频的是以下几个:

  • s:替换。sed 's/old/new/'默认只替换每行第一个匹配,要全部替换需加g标志,写成s/old/new/g。
  • d:删除。sed '3,5d'删除第3到5行,sed '/^#/d'删除所有以#开头的行。
  • p:打印。通常配合-n用,sed -n '/ERROR/p' file类似grep,但它可以做更精细的行号控制。
  • a:行后追加文本。sed '/end/a new line'在匹配行之后插入内容。
  • i:行前插入文本。sed '/end/i before line'在匹配行之前插入内容。
  • c:整行替换。sed '/pattern/c replacement'把匹配行整行变成新内容。

替换语法里的分隔符也可以换成#、@等,比如路径替换经常用sed 's#/usr/local#/opt#g',这样就不必对斜杠做转义了。&符号可以代表匹配到的原内容,比如把某个变量名包一层引号:sed 's/keyword/"&"/' file,结果会变成"keyword"。

3.3 实战:批量改配置、清空干扰行

我用得最多的场景是批量修改配置文件。比如有一批服务器上部署了旧配置,IP地址要统一从192.0.2.10改成198.51.100.23,端口从8080改成9090。直接手工改几十个文件会疯掉,而sed配合-i选项可以原地修改:

# 逐个替换,先改IP再改端口 sed -i 's/192\.0\.2\.10/198.51.100.23/g' config.ini sed -i 's/8080/9090/g' config.ini

注意IP里的点必须写成\.,否则会匹配任意字符,导致意外替换。这是新手最容易出事的地方。

我还经常用sed做“清理”工作:把文件里的空行和注释行去掉,方便后续解析:

sed -e '/^#/d' -e '/^$/d' config.ini

-e可以挂载多个脚本命令,执行顺序是从左到右。如果要用更复杂的逻辑,可以把多个动作写在一个花括号块里,比如:

sed '/^#/{s/^#//; s/^/comment:/}' config.ini

这表示对以#开头的行,先去掉第一个#,再在行首加上comment:。sed的语法虽然紧凑,但写多了会非常顺手。使用-i之前强烈建议先备份:sed -i.bak 's/foo/bar/' file,这样会生成一个file.bak的原文件副本。别问我为什么要强调备份,我见过太多改完才发现正则写错、已经没法还原的惨剧。

4. 数据流转:awk,让文本变成可计算的数据

4.1 awk 的基本架构:BEGIN、主体、END

awk定位就不是简单过滤了,它更像一个“微型数据处理语言”。awk处理逻辑分三段:BEGIN块在读取文件前执行,用来初始化变量或打印表头;中间主体块会对文件的每一行执行一次;END块在所有行处理完后执行,常用于输出汇总结果。标准写法是:

awk 'BEGIN { 初始化 } { 每行处理 } END { 收尾计算 }' 文件

awk默认把一行按“空白符”(空格或制表符)拆成多个字段,$1表示第1个字段,$2表示第2个字段,$0表示整行。NF是当前行的字段数量,NR是当前已读入的行号。如果你想按冒号或竖线拆分,就指定-F参数或设置FS变量。例如读取/etc/passwd时用-F:,因为它的字段分隔符是冒号。

4.2 内置变量与格式化输出

awk里最常用到的内置变量就几个:FS输入字段分隔符、OFS输出字段分隔符、NR当前行号、NF当前行字段数、$0整行、$1~$NF各字段。NR和NF经常被搞混,记住一句话:NR是“行号”,NF是“这一行有多少列”。比如awk -F: '{print NR, NF, $1}' /etc/passwd,会输出行号、字段数、用户名。

awk的print默认用空格分隔输出,如果你想输出“表格整齐”的效果,就上printf。printf的格式符和C语言类似:%s字符串、%d整数、%.2f保留两位小数。比如:

awk -F: '{printf "%s 的UID是 %d\n", $1, $3}' /etc/passwd

awk也支持完整的条件判断和循环,常见的写法是模式匹配加动作。awk '$3 > 100 {print $1}'表示只处理第3列大于100的行。这里的比较运算会自动把字段转成数值,非常方便。

4.3 实战:统计日志中的IP次数与金额汇总

awk最能体现价值的就是做统计。假设某访问日志每行第一个字段是IP地址,我想知道哪个IP访问最频繁,一行命令就能搞定:

awk '{count[$1]++} END {for (ip in count) print ip, count[ip]}' access.log | sort -k2 -nr | head -10

这里count[$1]++是awk里非常经典的“计数器数组”用法:以IP为下标,每出现一次就加1。处理完所有行后,for (ip in count)遍历每个IP并打印次数,再通过sort -k2 -nr按第二列数字倒序排列,head -10取前十。整个过程就是数据流转的典型示范。

再比如一个交易明细文件,字段格式是用户名|订单号|金额,现在要统计总金额、最大金额和订单条数,可以这样:

awk -F'|' '{if ($3 > max) max = $3; sum += $3; count++} END {print "条数:", count, "总金额:", sum, "最大单笔:", max}' orders.txt

-F'|'指定管道符为分隔符;if ($3 > max)用来找最大值;sum += $3累加金额。如果金额字段里混有货币符号或空格,比如$100.5,要先清理:awk -F'|' '{gsub(/[^0-9.]/,"",$3); sum += $3}',这个gsub可以把第三个字段里除了数字和点之外的字符全部删掉。日志处理中这种“脏数据清洗”非常常见,awk的gsub和sub函数就是为此而生的。

5. 协作:用管道与重定向实现数据流转

5.1 一键串成流水线

到了真正干活的时候,通常不是单独用某一个命令,而是把三板斧和排序、去重等命令串成一条管道。管道的含义很简单:前一个命令的标准输出接住后一个命令的标准输入。看这个链子:

grep -E "ERROR" app.log \ | sed 's/^2024-..-.. //' \ | awk '{print $1, $2}' \ | sort \ | uniq -c \ | sort -nr \ | head -20

这条命令做的是:先从日志里筛出ERROR行,然后sed去掉时间戳前缀,awk取出前两个字段,接着sort排序,uniq -c计数,再按次数倒序排列,最后取前20。每一步的输出都在变化:先是大文本,再是筛选后的行,再是精简字段,最后变成统计结果。这就是“数据流转”最直观的体现。

我习惯把长管道拆成多行来写,行尾用反斜杠\续行,这样不仅可读性好,排查问题时也能逐段验证。如果某一步输出不符合预期,可以把管道截断,先在上一段加个| head看看。

5.2 重定向的细节:覆盖、追加、错误流

管道处理的是命令之间的连接,而要把数据“落地”到文件,就要用重定向。>是把输出写入文件并覆盖原内容,>>是追加到文件末尾。这两者用错一次就可能出大问题,尤其是s脚本里执行错误的重定向,会瞬间清空原文件。我自己的习惯是:凡是写文件的操作,只要不是刻意清空,一律用>>,或者先执行到临时文件再改名。

标准错误的重定向也要学会。有时你想把错误信息也纳入同一个文件,可以写2>&1,意思是“把标准错误发送到当前标准输出指向的地方”。&>是bash里把两者都重定向的简写。常见的坑是运行脚本时只看到屏幕上的报错,但想日志里分析时怎么都找不到,就是因为没有重定向标准错误。

tee是个很实用的命令,它一边把数据写到文件,一边还能让数据继续流向后一个命令。比如grep ERROR app.log | tee error.txt | wc -l,既能把ERROR行存到文件,又能统计行数。这在需要“留底”又要实时计算时非常方便。

这里要特别提醒一个细节:管道传递过程中,如果中间的某个命令提前退出(比如head -5只读了5行就结束),上游命令可能会收到系统信号而异常终止。比如grep -r "关键字" / | head -1有时会刷出“Broken pipe”之类信息,这不一定是你命令写错了,而是管道自然断裂。了解这一机制的人不会被这种报错吓到。

5.3 完整场景:一个订单日志的清洗与汇总

把前面的知识点串起来,我用一个模拟场景彻底演示一遍。假设有一份order.log,每行以|分隔,格式是:

2024-06-01 10:00:00|user01|100.50|success 2024-06-01 10:05:00|user02|250.00|fail 2024-06-01 10:08:00|user01|30.00|success

现在要做三件事:只看success的订单;计算每位用户成功订单的总额;按总额从高到低输出。

grep "success" order.log \ | awk -F'|' '{amount[$2] += $3} END {for (u in amount) print u, amount[u]}' \ | sort -k2 -nr

这里grep负责“过滤行”,awk负责“分列求和”,sort负责“最终排序”。如果你还要把金额格式化成两位小数,可以再传入awk格式化。整个链条没有任何中间文件,数据从order.log一路跑到屏幕,效率很高。如果数据量巨大,这种流式处理的优势会更明显,因为内存占用非常小。

5.4 管道与子shell的一个经典坑

有不少人在编写shell脚本时会在管道里设置一个变量,结果发现在管道结束后变量是空的。例如:

cat file | while read line; do count=$((count+1)); done echo $count # 输出为空或0

原因是在bash中,管道两侧的命令都运行在“子shell”里,子shell中的变量修改不会影响当前shell。解决方法是使用进程替换,或者先重定向文件:

while read line; do count=$((count+1)); done < file

或者让循环体在管道直接输出。这个坑很隐蔽,但一旦理解“子shell”的概念,以后就能少踩很多雷。顺带说一句,写awk统计反而没有这个问题,因为awk本身就是独立进程,它会自己维护内部的变量状态。

6. 常见问题与排查技巧实录

6.1 grep 明明有内容,却匹配不到?

遇到“文件里能看到某个词,grep却搜不出来”的情况,先别怀疑人生,按顺序排查:

  • 文件可能是UTF-16或GBK编码,而终端环境是UTF-8。先用file 文件名看一下编码,用iconv转码后再搜。
  • 文本里有不可见字符,比如Windows下常见的CRLF换行。可以用cat -A 文件查看行尾是否有^M,用dos2unix或sed去掉\r。
  • 你搜索的内容里包含正则元字符,比如.会匹配任意字符,[]会当作字符集合。如果只查找普通文本但要包含这些符号,用grep -F "字符串"关闭正则解析。
  • 忘了加-i,但日志里是大写。

我最后再强调一次:怀疑匹配不上时,先cat -A看一眼文件真实内容,几乎所有诡异问题在裸露的原始字符面前都会无所遁形。

6.2 sed -i 在Linux和macOS上的差异

同样的sed -i 's/a/b/g' file在Linux上是没问题的,但放到macOS上会报错,因为BSD版sed要求必须带备份后缀。写法差异是:

# Linux GNU sed sed -i 's/a/b/g' file # macOS BSD sed sed -i '' 's/a/b/g' file

macOS里-i ''表示不生成备份文件,但那个空引号不能少。为了避免跨平台折腾,更稳妥的做法是写脚本时用sed -i.bak 's/a/b/g' file并显式指定备份后缀,这样两边都能接受。如果你在类Unix环境里要写可移植脚本,可以优先考虑用更简单的perl -pi -e 's/a/b/g' file,它的行为在大多数平台上是一致的。

6.3 awk 条件比较中的数字与字符串陷阱

awk里$1 > 100这类判断,默认会尝试把$1转成数值来比较。但如果字段里带了单位或者符号,比较值就会变得离谱。比如$1是50ms,awk会尝试从头解析数字,可能得到50,但如果开头就不是数字,则会当作0。解决方法是先清洗:

awk '{gsub(/[^0-9]/,"",$1); if ($1+0 > 100) print $0}' file

加上+0是为了强制把字段转为数值,避免字符串比较造成的奇怪顺序。字符串比较和数值比较的规则不一样,"100" > "99"在字典序下是false,因为“9”大于“1”,但在数值比较中就是true。这个原理一旦记牢,很多诡异的结果就解释得通了。

6.4 管道里的变量为什么消失?

前面提到过子shell问题。还有一个与管道相关的场景:echo "$var" | read也可能出现变量失效。很多人习惯用管道把内容读进变量,但read在管道里读入的数据无法传回当前shell。推荐改用IFS= read -r var <<< "字符串",或者直接命令替换。例如:

count=$(grep -c "success" order.log)

命令替换是把命令的输出作为变量值,这是最可靠的方式。需要提取多行数据时,可以用mapfile或临时文件。记住一条经验:在bash里,尽量少依赖管道去传递“变量值”,管道是用来传递“数据流”的。

6.5 文本文件的中文乱码与行尾统一

在日志分析、脚本输出中遇到乱码,多半是字符编码不匹配。查看文件编码用file -i 文件,转码用iconv -f 原编码 -t UTF-8 文件 > 新文件。很多Windows下生成的CSV文件,还会带着CRLF行尾,会让awk的最后一个字段莫名多出一个\r,导致比较失败。这时候先统一转一下:

sed 's/\r$//' input.csv > clean.csv

或者直接用dos2unix clean.csv。数据流转最忌讳的就是“脏格式”,所以我通常在进入排查流程时第一步就是做字符集和换行符的清洗。这也是为什么sed经常出现在管道的第二三段的原因。

7. 最后再说两句实践心得

我自己刚开始学这三板斧的时候,也觉得记忆量太大:grep一堆参数,sed语法像天书,awk更像一门编程语言。但后来发现,真正让我记住它们的不是看书,而是反复处理真实日志。你现在可以立刻打开一个日志文件或数据文本,先试试grep "关键字" 文件,再加-i -v -E,然后对结果执行awk '{print $1}',最后把这些输出接到sort和head后面。你会发现,原来一行命令真的能完成一个“迷你数据分析过程”。

如果让我给你一条最实在的建议,我会说:别追求一次记住所有参数,先把grep -E、sed -n s/p/d、awk '{print $N}'这三组最常用的写好,然后再一点点扩展。每次需要查新功能时,用man grep或man awk现场翻,但更重要的是结合场景去用,比如给历史命令的日志写个统计脚本,或者给某个配置文件写个批量修改命令。踩几次坑以后,那些曾经觉得难记的语法会变成你肌肉记忆的一部分。最后分享一个小技巧:在正式执行可能会改动数据文件的命令之前,先去掉-i跑一遍,把输出结果看一遍确认无误,再加回-i真正落地。看起来多了一步操作,实际上能救回很多次误操作。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进