☰
Linux文本处理五件套:grep、sed、awk、expect与正则实战指南
2026/10/19 5:05:46 网站建设 项目流程

做运维和开发这些年,命令行里最离不开的几把刀,就是 grep、sed、awk,外加一个正则在背后串着,偶尔碰到要自动登录、自动交互的活儿,还得再请出 expect。说它们是“文本处理五件套”一点都不夸张——日常排查日志、清洗数据、统计报表、批量部署,基本全靠这几样东西就能搞定,根本不需要动不动就上 Python 脚本。这篇文章会从五个工具各自的分工讲起,再把正则表达式里最容易踩的坑梳理一遍,最后用一个完整的日志分析案例把 grep、sed、awk、expect 串起来跑一次。适合刚接触 Linux 命令行的新手,也适合已经日常在用、但总在转义和引号上翻车的“老差生”。我尽量把话说得直接一点,少讲废话,多给能直接抄的写法。

1. 五个工具的分工:先搞清楚谁干什么,再谈组合

很多人学这几个命令,都是今天搜一个 grep 用法,明天抄一个 awk 脚本,用是能用,但一遇到复杂场景就不知道怎么拼。其实它们的关系非常简单:grep 负责挑,sed 负责改,awk 负责算,expect 负责交互,正则表达式是贯穿这几样东西的通用语法。理解了这个分工,你看到一个文本处理需求,脑子里就能立刻映射到对应的工具。

1.1 grep:最快的过滤刀

grep 的核心能力就一个字:筛。它从输入流里逐行读,把匹配正则的行挑出来。注意,它的最小单位是“行”,不是“词”,这是很多人一开始没转过弯来的地方。

日常用得最多的参数我也列一下:

  • -E:使用扩展正则(相当于 egrep),省去一堆反斜杠
  • -i:忽略大小写
  • -v:反向筛选,输出不匹配的行
  • -c:只统计匹配行数
  • -o:只输出匹配到的部分,而不是整行
  • -A/-B/-C:连同匹配行的后几行/前几行/上下文一起输出
  • -r:递归搜索目录

举个例子,查日志里所有报错且不是“健康检查”的行:

grep -E "ERROR|Exception" app.log | grep -v "health"

这条命令里第一个 grep 负责加大范围捞报错,第二个 grep 负责把噪音(健康检查的假报错)剔掉。这种“管道接力”的思路,就是整个文本处理体系的核心。

1.2 sed:流水线上的编辑工

grep 只负责挑行,但如果要在保留整体结构的前提下替换内容、删除某些行、插入新行,就要用 sed。sed 是流式编辑器,像一条流水线,读一行,按规则处理一行,然后输出一行。

我用得最多的是它的替换能力:

sed -n 's/旧/新/p' file

这行命令的含义是:关闭默认输出(-n),把匹配到“旧”的行替换成“新”,并且只打印被替换的行(p)。这里的 s 就是 substitute,末尾的 p 是 print。

sed 在默认情况下不会改原文件,只会把处理结果打到标准输出,只有加了-i才真正写回文件。这个设计初看反直觉,实际是个保护机制——你可以在正式执行前去掉-i试跑一遍,确认替换结果没问题,再落盘。

1.3 awk:把文本当表格算

如果说 grep 和 sed 是“按行”处理,awk 就是“按列”处理。它会自动把每一行按分隔符切开,第一个字段就是$1,第二个是$2,整行是$0。这天然适合做统计、求和、去重、格式化。

比如打印日志里每一行的第一列和第三列:

awk '{print $1, $3}' app.log

很多人觉得 awk 难,是因为它有自己的变量、条件、循环、数组,甚至有点像一门小语言。但实际日常使用,90% 的场景只需要三样:字段引用、BEGIN/END 块、以及一个数组做统计。后面实战部分我会用例子讲清楚。

1.4 expect:帮你“打字”的自动化助手

前三个工具处理的都是“静态文本”,expect 解决的则是“动态交互”——它能把用户在终端里手动输入密码、确认键、选择菜单这些动作自动化。原理很直白:它启动一个子进程(spawn),监听子进程的输出(expect),匹配到指定内容后就发送预设的输入(send)。

一个典型的 ssh 自动登录脚本是这样的骨架:

#!/usr/bin/expect set timeout 5 spawn ssh user@host expect "password:" send "your_password\r" interact

spawn起一个 ssh 进程,expect等它输出“password:”这个提示,send把密码发过去,interact把控制权交还给用户。就这么简单。

1.5 正则表达式:贯穿五者的通用语言

上面四个工具,没有正则在背后撑着,能力直接减半。但注意,不同工具里正则有不同的“方言”。grep 默认用的是基础正则(BRE),grep -E用的是扩展正则(ERE),grep -P才能用 Perl 兼容正则(PCRE),sed 默认也是 BRE,得加-r才进 ERE,awk 则天生用 ERE。这个差异,是无数转义问题的根源。我见过太多次有人在 grep 里写\d匹配数字,结果什么都没匹配到——因为 \d 是 PCRE 的写法,BRE 和 ERE 都不认,得写[0-9]。这类风格差异我会在下一节详细拆。

2. 正则表达式的关键知识点:别背语法,先理解机制

正则之所以劝退很多人,是因为网上教程喜欢把各种符号堆在一起,看起来像天书。但拆开看,它的核心只有三块:字符怎么匹配、数量怎么控制、匹配到的内容怎么分组引用。你把这三块搞透了,剩下全是查表的事。

2.1 字符匹配:从字面量到字符类

最基本的正则就是一个普通字符,比如error就匹配“error”这个字符串。但实际日志里,你经常要匹配“任意数字”“任意字母”“任意字符”,这时候就要用字符类。

  • .:匹配任意单个字符(换行符除外)
  • [abc]:匹配 a、b、c 中任意一个
  • [a-z]:匹配 a 到 z 任意一个小写字母
  • [^abc]:匹配除了 a、b、c 以外的任意字符
  • [0-9]:匹配数字,等价于大多数正则方言里的\d

这里必须强调一个新手高频误区:\d、\w、\s这一系列写法,来自 PCRE,在 grep 默认的 BRE 模式、以及grep -E和 sed 的默认模式里都不生效。只要你在 Linux 原生命令行里处理文本,老老实实用[0-9]、[a-zA-Z]、[ \t]这些字符类写法,才是最稳的。如果你实在想用 \d 这类简洁写法,可以给 grep 加-P参数,但注意它依赖系统里有没有 PCRE 库,有些精简环境的 grep 不支持。

2.2 量词:贪婪与非贪婪

正则里控制“出现次数”的符号叫量词:

  • *:前一个字符出现 0 次或多次
  • +:前一个字符出现 1 次或多次
  • ?:前一个字符出现 0 次或 1 次
  • {n}:恰好出现 n 次
  • {n,}:至少出现 n 次
  • {n,m}:出现 n 到 m 次

举例:colou?r可以同时匹配 color 和 colour,https?可以匹配 http 和 https,[0-9]{1,3}可以匹配 1 到 3 位数字。

量词默认是“贪婪”的,也就是会尽可能匹配更多字符。比如字符串"apple" and "orange",用".*"去匹配,正则会直接把"apple" and "orange"整段吃掉,而不是只吃掉两个带引号的单词。想让它短匹配,就得在量词后面加个?,写成.*?,这是 PCRE 的懒惰匹配语法,grep -P 支持,但 grep 默认的 BRE/ERE 和 sed 里都不支持。所以你在原生 grep 里做提取时,如果遇到贪婪匹配导致结果过长,最通用的解法是换一种“排除字符类”的写法,比如把".*"改成"[^"]*",它表达的是“除双引号以外的任意字符”,天然就只会吃到下一个双引号为止。

2.3 分组与引用:用括号做“捕获”

把一段正则用圆括号包起来,这个整体就会被记成一个“组”,后续可以用\1、\2这样的反向引用来引用它。这个能力最典型的应用是:提取内容、交换两段文本、做去重判断。

比如日志里的时间戳格式是2025-01-01 12:00:00,你想提取出“月-日”,可以写:

echo "2025-01-01 12:00:00" | grep -oE "[0-9]{4}-[0-9]{2}-[0-9]{2}" | sed -E 's/^[0-9]{4}-([0-9]{2})-([0-9]{2})$/\1-\2/'

这里 grep 先把完整日期捞出来,sed 再利用两个括号分组,取出月份和日期重新拼。看起来绕,但在日志分析场景里非常实用——你往往不想匹配整个文本,只想抽其中一小段。

2.4 三种正则风格对比:为什么你的正则换了工具就“失灵”

实操里我反抗最多的坑,就是正则在“不同工具”里的写法不同。这里整理一个对比表,你保存下来,遇到问题直接查:

语法grep 默认(BRE)grep -E / awk / sed -r(ERE)grep -P(PCRE)
分组\(和\)需要转义(和)直接使用直接使用
花括号次数\{n\}需要转义{n}直接使用直接使用
数字类不支持\d不支持\d支持
懒惰量词不支持不支持支持
环视(零宽断言)不支持不支持支持

看出规律了吗?BRE 里括号和花括号是“字面量”,想变成特殊含义必须加反斜杠;ERE 正好反过来,括号和花括号天然就是分组和次数控制符。你记不住时可以这样理解:grep 默认把(看成普通字符,你要是想分组就要给它“上力度”,加上反斜杠;加了 -E 之后,它才把括号当成真正的“分组符号”。

3. 实战:用一个日志分析需求,把五个工具串起来跑

理论说太多没用,直接上场景。假设你管着一套某公司的线上订单系统,每天产生大量访问日志,每行格式大概是这样的:

2025-03-21 10:23:45 [INFO] /api/order/create 200 user_id=1024 cost=123ms 2025-03-21 10:24:01 [ERROR] /api/pay/submit 500 user_id=1024 cost=987ms 2025-03-21 10:25:30 [ERROR] /api/order/create 503 user_id=2048 cost=0ms

需求很简单:统计 3 月 21 日当天,各个接口各出现了多少次 5xx 错误,按次数排序。看起来只是个小需求,但我故意把条件设得严一点,因为实际生产里的日志比这脏得多,正好把几个工具都带出来。

3.1 第一步:grep 把目标行捞出来

第一步永远是用 grep 把大文本缩小。先过滤出 5xx 响应码行:

grep -E " 5[0-9]{2} " app.log > 5xx.log

注意我用了5[0-9]{2}——前后都有空格,这是有意为之。如果不加空格,5999、15000 都会被误伤;加了空格,就强制“响应码是一个独立字段”。这种对匹配边界的敏感性,就是 street 经验和书本知识的区别。

如果日志里还有不少健康检查请求,它们的特征通常是路径固定,比如/api/health,可以顺手再剔除:

grep -E " 5[0-9]{2} " app.log | grep -v "/api/health" > 5xx.log

这里第二个 grep 的-v就是把包含健康检查的行整个扔掉。两条 grep 用管道一接,数据量从几十万行直接缩到几千行。

3.2 第二步:sed 把脏格式清洗规整

原始日志除了大部分规整行,还混着一些明显异常的脏数据。比如有的行时间戳后面多了个字段,有的路径里带了一堆引号,有的 cost= 后面没有单位。如果要喂给 awk 做统计,最好先统一格式。

第一个要处理的,是路径里被 URL 编码的字符,比如%20(空格)和%2C(逗号),它们在统计时会把同一个接口拆成两个截然不同的字符串。统一还原:

sed -i 's/%20/ /g; s/%2C/,/g' 5xx.log

sed 的s命令支持用分号分隔连续多个替换规则,所以这一行写完就能同时处理两类替换。-i直接改文件,跑之前我建议先去掉 -i 试跑一次,肉眼确认无误再落盘。

再处理另一个问题:日志里某些行的接口路径以双引号开头,比如"/api/order/create。不清理的话,awk 统计时同一个字段会带上引号。顺手脱掉:

sed -i 's/^"//' 5xx.log

^"只匹配行首的双引号,不会误伤路径中间的双引号。清洗这种东西,宁可多做几步,也不要指望一步到位——每步只做一件事,出问题才好排查。

3.3 第三步:awk 做字段切分与聚合统计

清洗完,就是 awk 的主场。目标:按“接口路径”统计 5xx 次数。字段顺序是:日期、时间、级别、路径、响应码、用户、耗时,所以路径是$4,响应码是$5。命令:

awk '{count[$4]++} END {for (path in count) print count[path], path}' 5xx.log | sort -rn

拆开看:count[$4]++的意思是,以第四列为 key 建一个关联数组,每出现一次就加 1。awk 的数组是“见谁建谁”,不需要提前声明。处理完全部行之后进入END块,用 for 循环把这个数组遍历一遍,输出次数和路径。最后管道给sort -rn,按数字倒序排。

跑出来的结果大概是:

37 /api/order/create 12 /api/pay/submit 5 /api/order/detail

这里再补一个更贴近实际的场景:如果你还想知道每个接口总共耗时多少,可以再加一个数组存总和:

awk '{count[$4]++; cost[$4]+=$NF} END {for (path in count) print count[path], cost[path], path}' 5xx.log | sort -rn

$NF是 awk 的内置变量,表示“最后一个字段”,因为耗时在行尾所以直接用这个取,不用数它是第几列。这就是 awk 在字段处理上的自由度。

3.4 第四步:expect 把重复操作自动化

现在你手上已经有了一套完整的分析命令,但是线上有两百台业务机器,不能登录一台敲一次。把命令封装成脚本,再用 expect 自动完成 ssh 登录、执行、退出,是最省力的做法。

先写一个分析脚本analyze.sh,内容就是前面几节的命令组合:

#!/bin/bash grep -E " 5[0-9]{2} " /var/log/app/app.log | grep -v "/api/health" | sed 's/%20/ /g; s/%2C/,/g; s/^"//' | awk '{count[$4]++} END {for (path in count) print count[path], path}' | sort -rn

然后写一个 expect 脚本,自动登录、执行、退出:

#!/usr/bin/expect set timeout 10 set host [lindex $argv 0] set password [lindex $argv 1] spawn ssh appuser@$host "bash /home/appuser/analyze.sh" expect { "password:" { send "$password\r" exp_continue } "yes/no" { send "yes\r" exp_continue } "eof" { } }

这里我用了exp_continue做循环匹配——首次登录可能弹出“yes/no”确认主机指纹,也可能直接要求密码,所以用 expect 的循环模式把两种情况都处理掉。等命令在远端执行完,ssh 进程自然结束,expect 就会捕获到eof而退出。你在本机再加一层for ip in $(cat ip_list); do ./run.exp $ip $pass; done,两百台机器的统计就自动化了。

这个例子里四个工具各司其职,grep 做第一道过滤,sed 清洗格式,awk 聚合统计,expect 包了一层自动化外壳。你把这套逻辑理解透了,遇到再复杂的文本需求,也能很快拆解成“筛、洗、算”三步。

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

工具用得越多,踩的坑越多。下面是我个人最常遇到的五个问题,每一个都附当时的排查思路,比网上随便搜到的“解决方案”实用得多。

4.1 转义地狱:shell 的引号和正则的转义叠加

这是新手和老手都会翻车的地方。正则本身要转义,shell 又有一套转义规则,两者叠起来经常把人绕晕。最典型的例子:想把日志里的 IP 地址的点号,替换成[.],sed 命令怎么写?

IP 匹配的正则是192\.168\.1\.1,点号必须转义,不然它会匹配任意字符。但你再塞进 sed 的替换里,还要再经过 shell 这一层。如果你用双引号包整个 sed 参数:

sed "s/192\.168\.1\.1/192[.]168[.]1[.]1/" file

这在双引号内是不会出问题的,因为双引号里反斜杠只在几种特定字符前起转义作用。真正容易出错的是你把正则写进单引号:

sed 's/192\.168\.1\.1/192[.]168[.]1[.]1/' file

这也是对的,因为单引号完全不做任何解释,反斜杠原样传给 sed。但你千万别在单引号里试图写变量,比如:

sed 's/$IP/192.168.1.1/' file

这里的$IP会被当成字面量,不会展开成变量值。想要变量就得切双引号:

sed "s/$IP/192.168.1.1/" file

但这时候$IP里如果有正则特殊字符,比如 IP 里的点,它又会被 sed 当成“任意字符”。这就是经典的转义叠加坑,结论很朴素:静态文本用单引号,变量文本用双引号,如果变量包含正则元字符,先用 sed/awk 把点号转义掉再拼接。标准做法是IP_ESC=$(echo "$IP" | sed 's/\./\\./g'),先转义,再造命令。

4.2 grep 匹配不到,但明明能看到内容

很多人用 grep 搜 IP、时间、中文,明明文件里肉眼可见有这个字符串,但 grep 就是搜不出来。90% 的可能性是:文件里不是纯文本,而是带颜色控制符、\r(回车符)、BOM、或特殊空白字符。

我遇到过最隐蔽的是 Windows 下生成的日志,每行末尾带\r,grep 搜行尾内容时怎么都搜不到。排查方法:

cat -A file

cat -A会把不可见字符显示出来,\r会显示成^M,TAB 显示成^I,行尾还有$记号。一旦确认有 CRLF 问题,用 sed 清一遍:

sed -i 's/\r$//' file

中文搜索不中的另一个常见原因,是当前终端的 locale 和服务端不一致。你可以先locale看一下,如果LANG不是 UTF-8,grep 中文字节序列时就容易出问题。把这个环境变量切到 UTF-8 再试:

export LANG=en_US.UTF-8

4.3 大文件卡死:性能和算法意识

很多人在几 GB 的大日志上直接跑while read line; do echo "$line" | grep xxx; done,结果是灾难性的慢。这相当于每读一行就起一个子进程,子进程还走一遍 grep 的初始化流程,几千行下来就让你怀疑人生。正解是:能一次过完的,绝不循环;能流式用的,绝不定数组。

常见的性能操作纪律:

  • grep 固定字符串时用-F,省去正则编译的开销
  • 只要不修改内容,优先 grep 而不是 sed 或 awk
  • awk 能在一次扫描里同时完成多组统计,别重复扫文件
  • 大文件不要频繁sort,必要时先用 awk 聚合到很小的结果集,再 sort

举个例子,一亿行日志统计每个接口的错误数,一条 awk 就够:

awk '/ 5[0-9]{2} / {count[$4]++} END {for (k in count) print count[k], k}' app.log | sort -rn

这比先用 grep 筛一遍再交给 awk 再扫一遍文件,快得不是一点半点——因为整个流程只有一次磁盘读。

4.4 awk 数组输出顺序乱:别以为 print 就是排序结果

awk 的数组是关联数组,输出顺序是随机的(确切说是哈希顺序),不代表文件里的出现顺序。很多人第一次跑聚合统计,看到输出顺序乱就怀疑自己写错了。这不是 bug,awk 本来就不承诺数组遍历顺序。想要按出现次数排序,就在外层套一个sort -rn,想要按 key 排序就sort -k2,千万别指望 awk 自己排好。我一般习惯在 awk 里只做统计,排序交给外层的 sort,职责分离,逻辑更清晰。

4.5 expect 脚本的使用陷阱:timeout、提示符匹配、特殊字符

expect 脚本看似简单,实际一跑就翻车的也特别多。先讲最常见的三个坑。

第一个坑是 timeout 设置太短。网络慢或者对端机器 load 高时,登录提示可能要好几秒才出来,你把 timeout 设为 2,expect 等不到就直接退出了。我一般设 10 到 15 秒,宁可多等,不要误判。

第二个坑是 expect 匹配的提示符不准。很多人以为 ssh 密码提示固定是password:,实际可能是Password:、Password for user@host:等。更稳的写法是把匹配写宽一点,比如用正则[Pp]assword,或者干脆匹配到@字符:

expect -re "@.*[Pp]assword"

第三个坑,密码里带特殊字符时send可能发错。比如密码里有$、[、],在 expect 的 send 里会被当作 Tcl 的特殊语法处理,导致密码发送错误。稳妥做法是用send -- "$password\r",其中--表示后面都不做特殊解释。我踩过这个坑之后,把项目里所有 expect 脚本都加上了--,再没出过密码问题。

4.6 sed -i 会不会把文件搞坏:先验证再动手

sed -i是直接改文件,一旦替换规则写错,文件内容就被冲了,而且没有 Ctrl+Z 的机会。我的习惯是无论如何,执行 -i 前先备份或者先试跑。试跑用sed 's/xxx/yyy/' file(不加 i)打印到屏幕,肉眼确认每一处替换都符合预期,再换成sed -i。另有一个加固技巧:

sed -i.bak 's/xxx/yyy/' file

这样 sed 会把原文件备份为file.bak,再做修改。虽然大多数时候用不上,但万一规则写错,一条mv file.bak file就能还原,成本几乎为零。这个习惯在自动化脚本里尤其重要——你根本没法预料线上数据的实际变形。

5. 结尾:一套顺手的工作流,比背一百个命令更值钱

我个人的体会是,真正让这几个工具发挥价值的,不是记住每个参数,而是形成一套“筛、洗、算、自动化”的思考方式。拿到一份脏文本,第一反应永远是:先用 grep 把需要的行捞出来,再用 sed 把格式洗到能算的程度,接着 awk 做字段级统计,最后如果这个流程要反复执行,就用 expect 或简单的 shell 包装一层,做成可复用脚本。

最后再分享一个小技巧:把你自己常用的统计命令写成 shell 函数,放到~/.bashrc里。比如我就在里边放了一个api_stat函数,接受日志路径作为参数,一键输出接口错误排行。这样你不需要每次重敲一长串管道,只要喊一声函数名,它就把 grep、sed、awk 那套流程全部跑完。工具链这东西,用熟了就是肌肉记忆,千万不要看着教程抄一步凑一步——把流程做成自己的“积木”,才是效率真正的拐点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询