Linux文本处理四大利器:grep、sed、awk、expect组合实战

📅 发布时间:2026/10/19 5:06:04
Linux文本处理四大利器:grep、sed、awk、expect组合实战
1. 为什么把这四个命令放在一起我见过太多人折腾 Linux 文本处理一个个命令单独拎出来都能用真到了实战场景却卡壳日志文件里查了一堆关键词下一步不知道怎么批量替换统计报表里的字段不知道用什么命令能直接算出总和写个自动化脚本连续登录几台机器每次都卡在密码输入上。这些痛点单个命令解决不了但把 grep、sed、awk、expect 按顺序组合起来基本能覆盖运维和开发中七成以上的文本处理需求。先说这四个工具的定位grep负责“找”从一堆文本里筛出符合条件的行。sed负责“改”对文本做替换、删除、插入、追加。awk负责“算”把文本拆成字段做统计、格式化、逻辑判断。expect负责“等”让脚本能自动应答交互式命令的提问。它们之间的关系你完全可以理解成一套流水线先用 grep 做初筛把关心的行捞出来接着用 sed 做清洗把脏数据换成干净的格式再交给 awk 做聚合计算得出最终结果如果整个流程需要连着跑多台机器、多个服务就用 expect 把中间的交互式确认全部自动点掉。这也是我平时处理日志分析、批量配置修改、自动化巡检最顺手的组合方式。这套组合还有个优势就是所有工具在几乎所有的 Linux 发行版上都预装了不需要额外装软件、不需要有图形界面、不挑硬件配置。对于刚入门的新手花一个下午把这四个命令的常用姿势过一遍之后遇到文本类需求基本都能自己搞定对于有经验的开发者这套组合的价值在于能替代大量临时写 Python 脚本的场景——一条 awk 命令就能做完的事情真没必要开一个脚本文件。2. 正则表达式在四个命令中的差异与选择正则表达式是这四个工具的通用语言但这里有个新手最容易忽略的问题grep、sed、awk 对正则的支持并不是完全一样的。很多人在 grep 里写好的表达式拿到 sed 里突然就不工作拿到 awk 里又变成另一种行为原因就在于它们默认采用了不同的正则规范。grep 默认使用基本正则表达式BRE在这个模式下、?、{、|、(、)这些符号都不具备特殊含义只是普通字符。想用这些功能要么给 grep 加上-E参数切换到扩展正则表达式ERE要么在符号前面加反斜杠做转义。sed 同样默认走 BRE所以你在 sed 的替换命令里想匹配“一个或多个数字”必须写成[0-9]\少一个反斜杠都匹配不到。awk 则完全不一样它从头到尾就使用 ERE加号就是加号、问号就是问号不需要额外的-E开关也不需要转义。我这里给你整理了一张对照表记下来能省掉很多排查时间功能grep (默认)grep -E / awksed (默认)匹配任意字符...字符集合[abc][abc][abc]出现一次或多次\\出现零次或一次\??\?或逻辑|分组\( \)( )\( \)行首锚定^^^行尾锚定$$$除了正则规范的差异还有一层转义问题容易被忽略你用双引号把正则传给命令行时shell 可能先对反斜杠做一层解释然后才轮到正则引擎。这就是为什么我建议在写比较复杂的匹配规则时统一使用单引号包裹正则单引号内的所有字符都会被原样传给命令不会经过 shell 的二次解释能少踩很多转义相关的坑。举个例子你想用 grep 匹配“error”出现一到两次的行# 在 ERE 模式下 直接生效 grep -E error app.log # 在 BRE 模式下必须转义加号 grep error\ app.log如果在这个例子里你用双引号包error\有些 shell 会先把\中的反斜杠吞掉最后正则引擎收到的反而是一个裸的字符在 BRE 模式下就只匹配普通的加号符号了结果自然是查不到任何内容。这种问题诡异得很但只要你养成“正则一律用单引号”的习惯基本不会再犯。3. grep 的进阶筛选姿势与实战细节grep 在很多人印象里就是grep keyword file但实际用起来有不少隐藏在参数里的能力。这里我不打算罗列所有参数手册只挑日常工作中回报率最高的几个用法。3.1 用 -o 提取匹配片段默认情况下 grep 打印的是整行内容有时候你只想知道匹配到的子串本身。比如从接口返回的 JSON 里抽出所有 IP 段用-o就非常方便cat response.log | grep -oE [0-9]{1,3}(\.[0-9]{1,3}){3}这条命令只输出匹配到的 IP 地址本身而不是带着前后文的一整行日志。后面再接sort、uniq -c就能直接统计哪些 IP 出现的次数最多一条管道就能完成过去需要写脚本才能做的频率统计。3.2 用 -A、-B 看上下文排查错误的时候只看错误行往往不够错误发生前几行的日志、错误发生后几行的堆栈信息才是定位问题的关键。-A表示匹配行之后的若干行-B表示匹配行之前的若干行-C则同时输出前后各若干行grep -C 5 ERROR app.log这条命令会把每条 error 前后 5 行一起捞出来。日志量特别大、错误又很密集的时候可以适当把-C调小到 2 或 3避免输出内容过多反而淹没了重点。3.3 用 -l 和 -L 在文件集合中搜索当你面对的不是单个文件而是一整个目录下的几十个日志文件想知道哪些文件包含某个关键字用-l只输出文件名比把匹配内容全打出来再手动翻找高效得多grep -rl timeout /var/log/myapp/反过来-L输出的是不包含关键字的文件列表这个在批量检查哪些配置没有生效时特别有用。3.4 用 -v 反向筛选-v把匹配逻辑取反输出不包含模式的行。比如过滤掉所有 debug 级别的日志grep -v DEBUG app.log | grep -E WARN|ERROR这里第一条 grep 把 debug 行全部丢掉第二条 grep 只留下 warn 和 error相当于用两个连续的 grep 做了一次简单的日志级别降噪。实际使用时还可以叠加--colorauto或-n-n会带上行号回到 vim 里修正问题时能少花不少时间。3.5 匹配多关键字的一个常见误区新手经常问 grep 能不能同时匹配多个条件。这里有两种场景容易混淆一是“满足任意一个条件就算匹配”用\|或-E的|二是“必须同时满足多个条件”多写一个管道再接一次 grep 就行。比如日志里要找出同时包含login和failed的行# 错误示例这表示匹配 login 或 failed grep -E login|failed app.log # 正确示例先筛 login再筛 failed两个条件都满足才输出 grep login app.log | grep failed这个差异我在不同团队都见过有人踩坑逻辑上其实就是集合里的“并集”和“交集”的区别但到了命令行里容易下意识写成一条命令结果漏掉了真正想找的日志。4. sed 的流式修改与原地操作sed 读作“流编辑器”它的设计思想和普通编辑器最大的区别在于“不进入交互界面按行处理”。sed 会一行一行读入内容对每一行执行你给它的指令然后把结果输出到标准输出。默认情况下它不会改动原文件只有当你显式使用-i参数时才真正做到原地修改。理解这一点你就明白为什么 sed 适合在管道里做数据清洗它不依赖打开文件、手动保存这些动作完全可以作为管道中的一个环节与其他命令自由串联。4.1 替换操作的完整写法sed最常用的子命令是s一个标准的替换写法是sed s/旧内容/新内容/标志 文件名标志位有几个常用的选择g表示全局替换不加的话只替换每行第一个匹配到的内容i表示忽略大小写数字如2表示只替换每行第二个匹配。实际工作中我几乎总是加上g否则很容易出现“改了第一处却漏了后面几处”排查的时候还以为是 sed 没生效。这里有一个需要特别提醒的细节当你的替换内容里出现/符号时比如替换路径/usr/bin为/usr/local/bin你可以换用#作为定界符sed s#/usr/bin#/usr/local/bin#g path.conf这样就不用写一串转义反斜杠命令的可读性和正确率都会好很多。4.2 原地修改与备份真正想改文件使用-ised -i s/old-config/new-config/g app.conf建议第一次做批量修改时顺手给原文件留个备份sed -i.bak s/old-config/new-config/g app.conf这个命令会先复制一份原文件为app.conf.bak然后再执行替换。我之所以强调这点是因为 sed 的替换并不像你肉眼看到的那行文本那么直接正则表达式的匹配范围一旦写得不准可能误伤本来不需要改的行。有备份在手改错了原地还原成本很低没有备份改错了想要恢复只能翻备份系统或者重新生成配置效率差太多了。4.3 按行号和处理范围操作除了内容替换sed 还能按行号处理。想直接删除第 10 行到第 20 行sed 10,20d data.txt想在第 5 行后面插入一行sed 5a\新增的内容 data.txt这里a是追加i是插入到指定行之前d是删除。组合起来还能做更精细的操作比如只处理匹配某个模式的行sed /^#/d config.conf这条命令删掉所有以#开头的行等价于清理配置文件中的注释。在批量修改一大堆配置时先清理注释、再做替换、再删掉多余空行用一条管道连环执行最终得到一份干净整洁的配置文件效率比一个个文件手动处理高出太多。4.4 经典“删除空行”写法删除空行是高频需求一行命令搞定sed /^$/d file.txt想同时删除包含空格的“假空行”换成sed /^[[:space:]]*$/d file.txt这个正则里的[[:space:]]是 POSIX 字符类匹配空格、Tab、换行等空白字符配合*表示零个或多个。这种写法比^ *$更严谨因为后者只匹配空格不匹配 Tab遇到用 Tab 缩进的空行就漏掉了。5. awk 的字段处理与统计计算awk 是整个组合里最接近“编程语言”的一个它有变量、有数组、有条件判断、有循环甚至还能定义函数。入门 awk 不需要把一个语言的全部特性学完只要掌握它的核心执行模型逐行读取文本默认按空白空格或 Tab把每行切割成若干字段然后用$1、$2… 去引用这些字段$0表示整行。基于这个模型awk 能做的统计汇总能力远超 grep 和 sed。5.1 字段切割与指定分隔符默认情况下 awk 用空格切分字段但日常处理的数据往往有更明确的分隔符比如 CSV 文件用逗号日志文件可能用 Tab 或|。指定分隔符用-Fawk -F, {print $1, $3} data.csv这条命令以逗号为分隔符取出第 1 列和第 3 列。我实际用的时候有个小技巧想要输出的列也能自己控制分隔符用OFS变量来指定“输出分隔符”awk -F, BEGIN{OFS\t} {print $1, $3} data.csv这里BEGIN{}是 awk 的特殊块它在处理任何数据之前先执行一次适合做初始化和预定义输出格式。上面的命令把第 1 列和第 3 列用 Tab 拼接后输出后续再接 sort 或 cut 都非常方便。5.2 条件匹配与取反awk 天然支持在行处理之前加条件表达式只处理符合条件的行。比如只统计状态码为 500 的请求数awk $9 500 {count} END {print count} access.log这里的$9是 Nginx 默认日志格式里的第 9 个字段也就是 HTTP 状态码。count每遇到一行符合条件的就加 1最后END{}块在数据全部处理完后执行一次输出统计结果。如果你想看非 200 状态码的行awk $9 ! 200 {print $0} access.log!是“不等于”这个写法能快速找出所有异常响应再把行内容完整打印出来配合前面的 grep 就能做多角度筛选。5.3 累加求和与平均值awk 做数字累加特别顺手。比如某个日志的最后一列是耗时单位毫秒想算所有请求的总耗时awk {sum $NF} END {print sum} request.log这里的$NF表示最后一个字段NF在 awk 里是一个内置变量表示当前行被切分出来的字段总数所以$NF就是按顺序取最后一个字段的值。如果还想同时算出平均值awk {sum $NF; count} END {print total:, sum, avg:, sum/count} request.log分号把两个语句放在同一行awk 照样按顺序逐行执行。这样的统计方式在没装 Python 或者其他脚本环境的最简机器上也能跑而且性能非常高几万行日志瞬间就出结果。5.4 去重统计与数组应用想统计某个字段不同值的出现次数用 awk 关联数组awk {count[$1]} END {for (ip in count) print ip, count[ip]} access.log这个命令把第 1 列比如来源 IP作为数组下标每出现一次就加 1。最后遍历数组输出每个 IP 和对应次数。在实际中我经常先跑这条命令得到粗略统计再管道接一个sort -k2 -nr按次数降序排序一眼就能看出哪些来源最频繁awk {count[$1]} END {for (ip in count) print ip, count[ip]} access.log | sort -k2 -nr | head -20sort -k2 -nr的含义是按第 2 列、以数值方式降序排列-n保证按数字而不是按字典序比较-r做反向排序否则 100 会排在 2 前面结果就完全错了。这种“awk 统计 sort 排序 head 取前几条”的管道结构我在日常分析中几乎每天都会用到。5.5 格式化输出awk 的printf能控制输出格式这在生成报表时很常用。比如保留两位小数awk {printf %.2f\n, $3/$4} data.txt我习惯在写脚本时把 awk 输出做成对齐的表格效果用printf %-15s %10d\n, name, count的方式固定列宽看起来比默认输出清爽很多。awk 的 printf 语法和 C 语言里的 printf 完全一样如果有 C 的基础基本零成本上手。6. expect 自动化交互应答前面三个命令处理的都是“文本流”但真实场景里文本处理往往不是孤立的你可能要连着跑好几台机器、执行命令、中途还得等到系统提示才输入密码或确认。碰到这种需求expect 就是最趁手的工具。它核心做的事情非常简单开启一个子进程捕捉进程输出中你指定的字符串然后自动发送预先写好的回复。6.1 核心概念spawn、expect、sendexpect 脚本的三个关键命令spawn启动一个外部程序或命令比如spawn ssh userhost。expect等待进程输出中出现某个模式一旦匹配成功就继续往下执行。send向进程发送字符串相当于模拟键盘输入字符串末尾通常要加\r模拟回车。一个最简单的自动化登录脚本长这样#!/usr/bin/expect set timeout 10 spawn ssh root192.168.1.10 expect password: send mypassword\r expect $ send uptime\r expect $ send exit\r第一行#!/usr/bin/expect指定了这个脚本直接用 expect 解释器执行。set timeout 10表示每个 expect 等待最多 10 秒如果 10 秒还没等到目标字符串脚本就超时继续或退出避免卡死。spawn后面就是要自动化的命令。expect password:的意思是等待子进程输出中出现 “password:” 这个字符串一旦出现就执行下一行send把密码发过去。后面每次执行完一条命令都先expect $等待命令提示符回来确保前一条命令确实执行完成了再做下一步。最后send exit\r断开连接。6.2 用变量和超时提升健壮性直接写死密码和主机 IP 的脚本不灵活改成变量更符合实际使用场景#!/usr/bin/expect set server [lindex $argv 0] set passwd [lindex $argv 1] set timeout 10 spawn ssh root$server expect { password: { send $passwd\r } yes/no { send yes\r; exp_continue } } expect $ send df -h\r expect $ send exit\r注意这里用了一个 expect 的变体写法大括号内写了两个匹配模式第一个匹配 “password:”第二个匹配首次连接时的 “yes/no”。当出现yes/no询问时先发送 yes然后exp_continue表示继续等待下一次匹配。为什么这条老要用exp_continue续上因为首次连接时系统往往先问 “Are you sure you want to continue connecting (yes/no)?”回答 yes 之后还会再问一次密码。不写exp_continue脚本在回答完 yes 后就结束了密码还没发出去整体流程就断了。$server和$passwd从外部传参./auto_login.exp 192.168.1.10 mypassword脚本可以复用不必为了每台机器改一次源码。后面看到连上之后执行的是df -h你可以替换成任意你想自动执行的命令组合比如批量查看负载、批量检查磁盘、批量抓取某条命令的输出都可以做成这样的模板。6.3 配合 sed 和 awk 做批量处理expect 的更大价值在于和前面的文本处理工具组合起来。当你有一份机器清单每台机器的 IP 和密码都写在文件里可以先写一个 shell 循环读取清单文件对每一行调用一次 expect 脚本执行远程命令再把远程命令的输出收集到本地文件中最后用 awk 做统计汇总。举个例子文件hosts.txt里每行是一台机器信息格式是IP 密码192.168.1.10 pass123 192.168.1.11 pass456 192.168.1.12 pass789在 shell 中循环处理while read ip passwd; do ./auto_cmd.exp $ip $passwd uptime result.txt done hosts.txt所有机器的 uptime 输出都保存在result.txt中之后awk {print $1, $3, $4, $5} result.txt就能把负载信息和时间戳全部整理成一张简洁的汇总表格。这就是整套组合的真正威力grep 负责初始筛选sed 负责格式清洗awk 负责聚合加工expect 负责把分布在不同机器上的操作串成自动化流水线四种工具各司其职又彼此衔接。6.4 expect 的常见执行方式与注意事项expect 除了脚本文件还能直接在命令行中用expect -c来执行单条命令适合临时性的自动化任务expect -c spawn ssh root192.168.1.10; expect password:; send pass\r; expect $; send exit\r这种写法的可读性不如脚本文件但好处是快、用完即走。我建议把它用在写一次性调试脚本的场景里长期维护还是用独立的.exp文件更好。使用 expect 时还有几个注意事项密码以明文形式写在脚本中脚本文件权限要限制好至少chmod 600不要放在能被其他用户读到的目录。expect的匹配是子串匹配expect password:也会匹配到Password:这种大小写混写的情况但如果你写的模式和系统实际输出的内容有差异脚本会一直等到超时。遇到这种情况优先把timeout调小一点做快速失败更容易排查是哪一步匹配没等到。在多台机器批量执行时网络波动或者命令输出延迟都可能导致某台机器提前超时。我一般会在 expect 脚本最外层加一个循环重试机制比如第一次超时就重新连接最多重试三次大幅提高整体成功率。7. 常见问题与排查技巧实录我把使用这套组合时最常遇到的问题整理成了速查表每条都标了现象、原因、解法你在实操中卡住时可以按表排查。现象原因解决办法grep 正则里的匹配不到任何内容grep 默认是 BRE 模式是普通字符加-E参数或把改成\sed 执行替换后文件内容没有变化sed 默认不改原文件只输出到屏幕确认加了-i参数sed -i 把不该替换的行也改了正则写得太宽比如s/a/b/g中间没有限定边界在正则中增加锚定^、$或字符边界\bawk 中 $1 总不是自己想要的列文件里的分隔符不是空格用-F显式指定分隔符如-F,awk 数字统计结果变成了字符串拼接某个字段在个别行不是数字printf 或字符串拼接影响格式需要的时候用0强制把字段转成数值如$10expect 脚本刚启动就退出没有正确写脚本第一行解释器或文件没有执行权限第一行写#!/usr/bin/expect然后chmod xexpect 一直等到超时匹配的模式和实际输出不匹配比如少了冒号或多了一个空格先用expect加debug模式观察子进程输出或者手动执行一次命令看提示符长什么样管道里使用sed -i报错sed -i作用于输入流时行为不稳定需要临时文件替换不要对管道输入直接-i先重定向到临时文件再处理我再补充一个具体排查案例。有一次处理一批配置文件原本想把所有Listen 8080改成Listen 9090用的是sed -i s/Listen 8080/Listen 9090/ httpd.conf结果发现文件中所有带“Listen 8080”字样的备注行也被替换了因为正则“Listen 8080”会匹配任意位置出现的这串字符并不要求它单独占一行。改成锚定行首的写法后问题立即解决sed -i s/^Listen 8080/Listen 9090/ httpd.conf^把匹配锁定在行首备注行头部不是 Listen就不会被误伤。这类“正则写得太宽”的问题在 sed 中高发排查时从“这个模式在实际文本里还会匹配到哪些位置”的角度去审视往往很快能找到原因。8. 个人实际使用中的几点体会这套四件套我的使用频率非常高慢慢也总结出一些书本上不常写、但实际很管用的经验。第一测试阶段永远先用不带-i的命令看输出。不管 sed 想怎么改文件先跑一遍看结果打印出来是什么确认无误后再加-i真正落地。这个习惯我吃了不少亏才养成尤其是在一次要处理几十个文件的时候改错一个再找回原文件的时间成本远高于多敲一条命令的几秒钟。第二整个命令行处理流程务必养成管道、重定向和临时文件的组合习惯。我自己的固定套路是把 grep 过滤后的中间结果先存到一个临时文件再对临时文件做 sed 和 awk 处理而不是一次性写一个超长管道。这样好处很明显每个阶段都能独立验证出问题时可以快速定位到底哪一步的结果不对。等到流程完全跑通再合并成一条管道给生产环境使用。第三能用一条命令解决的不要急着写脚本。有些时候 awk 一条命令就完成的需求被写成几十行 Python从执行效率和维护成本上看都不是最优解。但反过来如果发现命令已经复杂到肉眼几乎无法读懂就该停下来把它整理成脚本了。这个平衡点我个人的标准是超过三个管道符或者中间步骤超过两层就拆成脚本文件至少在注释里写清每一步在做什么。这套组合的学习路径也不一定要线性。先掌握 grep 的常用参数和正则基础再上手 sed 的替换与删除然后花点时间啃 awk 的字段切分和统计数组最后用 expect 串起整个自动化流程。每一步都在解决实际问题的过程中自然推进很难忘记。我是真心建议你直接拿一份真实的日志文件当练习材料边学边跑比对着书背参数有效率得多。