Linux文本处理三剑客:grep、sed、awk实战指南
1. 先把话说清楚这三个命令到底解决什么问题1.1 一个真实场景从日志中找到问题的30秒有一次我在线上环境排查接口超时同事甩给我一个几十万行的access.log丢下一句“你帮我看下最近5分钟哪些IP请求量异常”。我当时的反应不是打开编辑器慢慢翻而是手速很快地敲了一串命令30秒内把结果直接贴给了他。那串命令里用到的核心工具就是今天要聊的文本处理三剑客grep、sed、awk。很多刚接触Linux或者Shell的朋友会有一种误解觉得grep、sed、awk是“老古董”平时也就用个grep找找东西sed和awk基本不敢碰。但实际上只要你还在用命令行这三件套就是性价比最高的投资。它们不依赖图形界面不依赖Python环境几乎每台服务器上都内置处理纯文本和日志的速度远超你用脚本读文件的常规写法。这篇文章不会堆砌命令手册而是从“我实际会怎么用”的角度把grep的搜索、sed的编辑、awk的统计串起来。无论你是刚入行的运维、后端开发还是经常和日志打交道的测试同学看完都应该能直接上手并且能理解它们背后的执行逻辑而不是死记硬背参数。1.2 三把工具的分工搜索、修改、统计先说一个很朴素的划分grep负责“找”sed负责“改”awk负责“算”。这个划分当然不够精确但足够指导新手入门。实际工作中你会看到它们经常配合使用——先用grep滤出关心的行再用sed做格式调整最后用awk做聚合统计。grep按模式匹配行输出命中的行。它是文本过滤的第一道闸门。sed流式编辑器对输入流逐行处理擅长替换、删除、插入、行范围操作。它和交互式编辑器的最大区别是“非交互、自动化”。awk把一行拆成多个字段支持条件判断、循环、数组和函数本质上是一门小而完整的文本处理语言特别适合做统计报表。这三者还有一个共同点都是逐行处理文本流。你给它们一个文件或者从管道喂数据它们吐出来的还是处理后的文本流。理解“流”这个概念后面所有命令都好解释了。1.3 别被“命令”两个字吓住它们的基础是正则和“行”学三剑客最大的心理障碍其实是正则表达式。但入门并不需要把正则的所有规则背下来你只需要掌握最核心的一小撮. * [] ^ $ \大概能覆盖90%的日常匹配需求。剩下的规则在遇到具体问题的时候再查就行。另外一个关键概念是“行”。grep一行一行判断sed一行一行处理awk一行一行拆分所有的行动单位都是“换行符隔开的行”。一旦你记住了这两件事——正则和行三剑客的骨架就搭起来了。2. grep搜索是第一生产力2.1 grep最常用的参数其实翻来覆去就那么几个我见过不少人用grep只会写grep abc file其实它的常用参数组合起来能力会大很多。先说我最习惯的一套组合grep -n ERROR app.log grep -v ^# nginx.conf grep -i timeout app.log grep -r TODO src/ grep -c 404 access.log grep -E ERROR|WARN app.log-n显示行号。排查问题的基本操作没有行号你根本没法定位。-v反向匹配。过滤掉垃圾信息的时候非常有用比如看配置时忽略注释和空行。-i忽略大小写。日志里有时是Error有时是ERROR加上它省心。-r递归搜索目录。改代码找引用的时候比在IDE里一个个点快。-c只输出匹配行数。统计类需求直接起飞。-E扩展正则。配合|做多模式匹配一条命令替代好几条。如果你还想在匹配的同时看看上下文用-A、-B、-C。比如grep -C 3 ORA-01555 alert.log把错误前3行和后3行都打出来定位问题快得多。这几个参数我几乎天天用强烈建议背下来。2.2 grep在shell脚本中的常见用法不只是打印结果热搜词里专门有一条“grep在shell脚本中的常见用法”这点确实值得单独讲。因为很多新手把grep当成“能看就行”的命令但在脚本里grep最值钱的用法其实是“条件判断”和“信息抽取”。第一种用法判断文件是否包含某段内容。利用grep的退出码——找到返回0没找到返回1配合if判断不用再写复杂的循环比对if grep -q ServiceUnavailable status.log; then echo 服务不可用触发重启流程 restart_service else echo 服务状态正常 fi注意这里用了-q静默模式不输出匹配内容只返回退出码。在脚本里做判断时-q几乎是必备的否则每次执行都会打出一堆无关输出。第二种用法从文本中抽取关键信息。比如从配置里取某个键的值配合cut或awk使用grep ^max_connections my.cnf | cut -d -f2这句是经典的“取配置值三板斧”先用grep按行匹配到配置项再用cut按分隔符取字段。这段代码在自动化部署脚本里反复出现算是没有awk参与时最朴素的写法。第三种用法统计日志中的特定错误次数。适合做监控告警逻辑error_count$(grep -c ERROR app.log) if [ $error_count -gt 100 ]; then echo 错误数超阈值 fi这里$(...)是命令替换把grep的输出结果赋给变量。脚本里统计错误率、判空、做比较基本都是这套模式。2.3 正则没你想的那么难但有一个坑必须知道先用一个最速成的表格覆盖基础元字符元字符含义示例.任意单个字符a.c匹配 abc、adc*前一个字符重复0次或多次ab*c匹配 ac、abc、abbbc^匹配行开头^ERROR匹配以ERROR开头的行$匹配行结尾END$匹配以END结尾的行[]字符集合[0-9]匹配任意数字\转义符\.匹配字面点号这里有一个新手必踩的坑在基础正则里|和、?不是随便能用的。|是OR但默认情况下grep认为它是普通字符你要么用grep -E要么写成\|。和?也是同理-E可以让你写正则的姿势更现代。建议你从一开始就习惯用grep -E而不是纠结基础正则和扩展正则的差异。第二个坑是中文环境中容易出现的问题文件是GBK编码时直接grep中文会乱码或匹配不上。现在的服务器基本都用UTF-8但偶尔碰上古董文件可以用iconv -f GBK -t UTF-8 file | grep 中文先转编码再搜这算是我踩过后的补充经验。2.4 grep的边界什么情况别再硬用grep虽然好用但不是银弹。我见过有人用grep a.*b去匹配跨行的内容最后匹配了个寂寞。grep是逐行匹配的它不会跨行。跨行匹配用awk更合适或者先把换行符替换掉再处理。另外当你需要“精确取字段”而不是“按行过滤”时grep就显得粗糙了。比如你想取出日志里的访问耗时并计算平均值grep只能把整行给你后面的处理还是要靠sed或awk。所以grep的正确位置是“第一道过滤网”而不是“最终加工车间”。记住这个定位你就不会在它身上花太多无用功。3. sed很多人的第一反应是替换但本质是“流”3.1 流式编辑器和一个常见的错误预期理解sed最有用的类比是把它想成“一条流水线”文件的内容像传送带上的零件一行一行地流经sed的工位sed按你给它的指令对每一行做加工完成后继续送往下一个工位。是处理完一行就丢掉一行而不是像Vim那样先把整个文件读进内存、等你慢慢编辑完再统一保存。所以sed处理几十万行的日志文件内存占用很低速度飞快。我第一次用sed时犯过一个错误把sed当成“编辑器”总觉得它会把文件改成我想要的样子。但实际上默认情况下sed只是把处理结果输出到屏幕文件本身根本不会变。想让文件改变你要么用重定向放进新文件要么用-i参数在原文件上直接操作。这个观念转过来sed就算入门一半了。3.2 寻址sed的“指哪打哪”sed的基本语法是sed 寻址命令很多教程直接讲命令忽略了寻址导致读者一开始就懵。其实寻址就是“你想让sed对哪些行下手”有三种最常用的写法sed -n 5p file.txt # 只打印第5行 sed -n 10,20p file.txt # 打印第10到20行 sed /ERROR/p file.txt # 打印匹配到ERROR的行第一种是行号寻址第二种是行号范围第三种是模式寻址。注意模式寻址用正则前后都有斜杠这是sed最常用的形式配合/^$/d删除空行、/^#/d删除注释行都是配置文件处理的经典操作。在“打印”这个场景下-n参数是必须的。因为sed默认会把所有行打印一遍加-n相当于关掉了默认打印只输出你显式用p指定的行。新手最容易漏掉-n结果看到满屏文件内容加重复输出还以为命令写错了。3.3 替换的完整写法以及那些容易出错的细节sed最招牌的功能是替换基本格式是sed s/旧内容/新内容/一个常见的实例是把配置文件里的端口号改掉sed -i s/8080/9090/ nginx.conf这句很容易看懂但有几个细节是我在实际使用中摸出来的第一默认的s只替换每行第一次出现的内容。如果一行里多次出现8080你需要加g标志sed s/8080/9090/g。我见过有人在替换IP白名单时漏了g结果每行只改了一半整个配置直接废掉。第二如果你的替换内容里有斜杠比如要替换路径/usr/local/bin再写/做分隔符会非常痛苦需要各种转义。这时可以换分隔符比如sed s|/usr/local/bin|/opt/bin|g。只要分隔符后统一就行随便换成#、都行。第三替换内容里的有特殊含义它代表“匹配到的整个内容”。比如你想给所有数字加个方括号echo abc 123 | sed s/[0-9]*/[]/ # 输出 abc [123]这句在一些自动生成代码的场景里非常有用。如果你确实想输出字面量的需要写成\这也是一个比较容易踩的转义点。3.4 模式空间与保持空间sed工作流程的秘密热搜词里有“sed 保持空间 模式空间”这个必须讲清楚因为它是理解sed复杂脚本的门槛。你可以这样理解sed的处理流程从输入流中读取一行把它放进“模式空间”。对模式空间中的内容执行你给出的命令。默认情况下把模式空间的内容输出。清空模式空间读取下一行重复上述过程。所以模式空间就是sed每次处理一行时随身携带的“工作台”这上面放着当前行所有操作都在这里完成。默认处理完就腾空迎接下一行。而保持空间则是一个“临时仓库”它不会随着每行处理完就自动清空。你可以用h命令把模式空间的内容复制到保持空间用G命令把保持空间的内容追加回模式空间相当于在行与行之间传递信息。这套机制可以完成什么奇技淫巧最经典的是倒序打印文件sed -n 1h; 1!G; h; $p file.txt这个命令逻辑拆开看第一行时执行h把内容存入保持空间从第二行开始先G把保持空间内容追加到模式空间再用h把当前这行存进保持空间循环到最后打印出来就实现了倒序输出。说实话这种写法在实际工作中并不常用但理解模式空间和保持空间能帮你读懂别人写的高级脚本而不是面对一串sed命令一脸懵。另外要注意G与g不同G是把保持空间内容追加到模式空间末尾并加一个换行符g则是用保持空间内容覆盖模式空间。大小写之差结果天壤之别。这个细节我在初学时栽过跟头打印结果多了一行空行排查了半天才明白是G和g用错了。3.5 sed -i的食用注意事项-i参数意味着原地修改文件这个参数一旦用错轻则配置文件被改坏重则文件内容被清空。我给你的建议非常朴素在命令执行前先不加-i跑一遍看看输出结果符不符合预期确认无误后再加-i执行。如果你用的是GNU版本的sed强烈建议养成加备份后缀的习惯sed -i.bak s/8080/9090/g nginx.conf这会在生成新文件的同时留下一个nginx.conf.bak备份。版本发布时如果改坏了直接mv回来就恢复了成本几乎为零。虽然多了一步但在生产环境上这一步能救命。还有一点sed -i在GNU sed和BSD sedmacOS自带上的语法略有差异macOS要求必须有后缀参数sed -i s/8080/9090/g file。如果你在mac上开发、在Linux上部署这句差异足够让你在联调时怀疑人生。4. awk从“第几列”到“自己的临时语言”4.1 awk和grep/sed不是一个量级它是迷你编程语言如果grep是“初筛漏斗”sed是“流水线工人”那awk就是“现场数据分析师”。awk本身是一门完整的编程语言它的内置变量、数组、字符串函数、数值计算能力足以支撑你在命令行里完成一次的小型数据报表。awk的核心理念是“按列处理”。“列”这个词在awk里叫字段。默认情况下awk用空白空格或Tab作为字段分隔符把每一行拆成$1、$2、$3……比如你有这样一行192.168.1.10 GET /index.html 200 0.032用awk处理$1就是IP$2是请求方法$3是URL$4是状态码$5是耗时。$0则代表整行。字段的个数用内置变量NF表示$NF就是最后一个字段。这个设计极大简化了“取某几列”的操作。我还记得第一次看别人用一长串awk命令统计日志时第一反应是“这也太玄学了”。但当我理解了$NF代表最后一个字段、NR代表当前行号后再看那些命令就豁然开朗这不就是“SQL表”的迷你版吗一行就是一条记录字段就是列条件就是WHERE数组加END就是GROUP BY。4.2 字段分隔符FS、OFS以及一个经典坑awk默认用空白分隔但在日志文件里很多字段是用逗号、竖线或冒号分隔的此时需要指定-F参数awk -F: {print $1} /etc/passwd awk -F, {print $2} data.csv awk -F| {print $3} raw.txt注意一个细节如果你用竖线做分隔符通常要写成-F|。这里如果你直接写-F|在某些shell下会出错因为|是管道符shell会先把它解释成语法符号。加了引号就安全了所以建议所有-F的参数都统一带上引号别省这个习惯。与FS对应的是OFS输出字段分隔符。默认情况下awk打印多列时用空格分隔如果你想把结果变成逗号分隔的CSV只需要设置OFSawk -F, BEGIN{OFS,} {print $1, $2*2} data.csv这个BEGIN块很关键它在处理所有行之前执行一次适合做初始化。关于BEGIN和END下面一节详细展开。4.3 BEGIN和END在开头做点事在结尾算个账awk有两种特殊模式是所有报表的骨架BEGIN{}在所有行处理之前执行END{}在所有行处理完之后执行。举个最有代表性的例子统计access.log中所有GET请求的平均耗时。awk /GET/ {sum $NF; count} END {print avg:, sum/count} access.log逐段拆解/GET/是模式匹配只处理包含GET的行{sum $NF; count}是动作把最后一个字段的耗时累加到sum并统计行数END {print avg:, sum/count}是收尾计算输出平均值。三个部分一组合一条秒级的统计命令就出来了。用BEGIN初始化一个变量或者设置格式常量用END来输出汇总结果这是awk最value的用法。我做接口压测时经常这么干awk {if($NF 200) slow} END{print slow, 个慢请求} perf.log当然awk也支持条件过滤和数值比较这块的写法非常像C语言的if判断后面我还会提到。4.4 几个能直接抄作业的模板分组统计、条件汇总、格式化输出直接给你几个我运维和测试中反复使用的模板抄完改改就能用。按状态码分组统计个数awk {count[$NF]} END {for (code in count) print code, count[code]} access.log这个用到了awk数组。count[$NF]的意思是用状态码作为数组下标每次遇到就自增。遍历的时候用for (code in count)取出所有键打印统计结果。注意awk的数组是关联数组下标可以是数字也可以是字符串这一点非常灵活。很多人在此之前还只会用sort和uniq其实awk一行就完成了分组统计。按IP统计请求数并输出最多的前5个awk {ip[$1]} END {for (i in ip) print i, ip[i]} access.log | sort -k2 -rn | head -5这条命令是awk和sort的组合拳。awk负责分组计数sort负责按第二列数值逆序排序head负责取前5行。掌握了这个结构你就能回答几乎所有的“Top N”类问题。条件汇总加格式化输出awk { if ($9 500) { server_error; total $NF; } } END { printf 5xx请求数: %d\n5xx耗时总和: %.2f\n, server_error, total; } access.log这里用到了printf格式化输出%.2f控制小数位。如果你要生成监控报表这种格式化能力会让你少写很多拼接字符串的代码。最后提醒一下awk的字段比较不需要引号比如$1 ERROR可以直接写但如果比较值是数字千万不要加引号$NF 100才对$NF 100变成了字符串比较结果会让你怀疑人生。4.5 awk里的变量和函数不需要精通但要知道它行awk除了内置变量还能自定义变量、调用内置字符串函数。常用的内置函数我用一个表格整理函数作用示例length()计算字符串长度length($1)substr()截取子串substr($4, 1, 8)split()按分隔符拆分字符串到数组split($NF, arr, :)toupper/tolower大小写转换toupper($2)比如从日志的时间戳2024-01-05T12:33:22中提取日期部分可以写awk {print substr($4, 1, 10)} access.log这算awk派上用场的经典例子。但说句实在话我在日常使用中并不会死记硬背这些函数每次用到的时候查一下man手册或网上资料就够了。关键是你得知道“awk可以做这件事”知道它在需要时能顶上这就比完全不知道要强得多。5. 三剑客的合击一个日志分析案例的完整思路5.1 需求找出最近日志中耗时最高的GET请求讲完三个工具各自的用法我用一个相对综合的小需求把串联起来。假设我有这样一个access.log每行格式如下2024-03-02 10:15:22 192.168.1.12 GET /api/order 200 0.452 2024-03-02 10:15:23 192.168.1.13 GET /api/user 200 0.113 2024-03-02 10:15:24 192.168.1.14 POST /api/pay 500 1.342需求是找出耗时超过1秒的所有GET请求按耗时降序排列只输出时间、IP、URL和耗时。这个需求在面试或日常排障中很常见用三剑客来解非常顺。5.2 一步一步拆解每个工具在这条命令里干什么第一步用grep把GET请求过滤出来grep GET access.log但这里有一个问题grep是纯文本匹配它不关心字段位置。如果某行URL里恰好出现“GET”字样比如/gateway/也会被误匹配。更好的做法是用awk来按字段过滤因为awk天然区分“第几个字段”awk $2 GET access.log这体现了一个很重要的思路能用字段精确判断的尽量不要用全文匹配。字段判断比全文搜索要准确得多这也是awk排在grep后面的原因——它不是替代品而是更精细化的一层。第二步过滤出耗时超过1秒的请求awk $2 GET $5 1 access.log注意这里$5是第5列0.452awk的数值比较会自动把字符串转成数字。耗时超过1秒的1.342会顺利通过。第三步按耗时降序排序并输出关键列。排序用sort取列用awkawk $2 GET $5 1 access.log | sort -k5 -rn | awk {print $1, $2, $3, $5}这里sort -k5 -rn表示按第5列数值降序排序。注意管道后面的awk没有过滤条件只做了取列输出。很多新手会想为什么第一步不直接把所有事干完因为awk虽然能做统计和条件但“排序”不是它的强项。awk的数组END遍历输出顺序是随机的没有sort的-rn来得直接。5.3 从“一条长命令”到“脚本化”上面的命令如果只是临时用一次一行搞定没问题。但如果这个分析每天都要做比如生成定时报告我建议封装成一个shell脚本并把日志路径做成参数#!/bin/bash log_file${1:-access.log} threshold${2:-1} awk $2 GET $5 $threshold $log_file | sort -k5 -rn | awk {print $1, $2, $3, $5}这里有一个shell和awk混用时容易踩的引号陷阱awk的脚本部分用了单引号包着{...}如果需要在awk外部传入变量你要么先拼接出来要么用-v传参。上面写法里$threshold是先关闭单引号、再插入shell变量、再重新开单引号很容易敲错。更推荐的做法是用-vawk -v th$threshold $2 GET $5 th $log_file-v th$threshold把shell变量传给awk内部变量th脚本内部就不需要再纠结引号了。这个写法我要重点推荐比拼接单引号优雅得多也基本不会出错。5.4 把输出结果变成可读的报表如果只是输出几列数据其实已经满足需求了但实操中我经常会再加工一层加表头、算平均耗时、统计总数。合并起来可以写成awk -v th$threshold $2 GET $5 th { count; sum $5; printf %s %s %s %.3fs\n, $1, $2, $3, $5; } END { printf ----\nslow_count%d, avg_time%.3fs\n, count, count ? sum/count : 0; } $log_file | sort -t -k4 -rn注意这里的printf格式串里用了%.3fs将就显示耗时单位。如果你不想把格式搞复杂也可以先输出纯数字最后用sort排完再交给awk补格式。我的习惯是先让数据快速算出来再慢慢排版。命令是一步步长出来的不是一开始就写完整。6. 我最想告诉你的几个经验踩坑和习惯6.1 引号和转义三剑客80%的语法错误都源于此我先说引号。grep、sed、awk的命令里都会出现正则表达式而正则里的元字符如果被shell先解释掉结果就是“看起来没报错但匹配的东西完全不对”。最典型的场景是$符号在shell里$代表变量引用。如果你写grep error$ file在双引号里shell不会处理$没问题但如果你写成单引号grep error$ file单引号内所有字符都会被当成字面量shell同样不会处理也行。真正容易错的是你混用引号或者忘了引号grep error$ file # $会被shell解释匹配的东西变成变量值结果无法预测 sed s/^#// config # 命令里的空格会导致语法断裂我的经验是在命令行里写的正则全部用单引号包裹。原因很简单单引号内的内容原样交给命令不需要考虑$、*、!这些shell元字符。但引号也有一个例外当你确实需要在命令中插入shell变量的值时单引号就不方便了。此时用双引号或者-v去传参别硬拼。6.2 编码和区域设置中文环境的隐形杀手我处理过的日志有很多是中文内容。在中文locale下grep处理UTF-8文件通常没问题但awk的length()统计中文时不同awk版本计算出的“字符数”不一致。我踩过的具体坑是用length()判断字段长度来做校验结果在本地是3到服务器上变成了6脚本直接失效。后来发现是locale的影响。建议在脚本头部统一设置环境变量export LC_ALLC不过LC_ALLC也会带来一个副作用你没法用[[:alnum:]]这类POSIX字符类去匹配中文因为C locale下正则按字节匹配中文会被拆成多个字节。所以我的选择是纯英文日志用LC_ALLC保证性能中文日志则在UTF-8环境下处理不用length()去数中文字符。6.3 先验证再执行在安全和效率之间找到平衡grep和awk默认只是输出到屏幕安全性尚可但sed -i和awk的重定向修改文件就要小心了。我的习惯动作是先不写-i把sed的输出打到屏幕看看。对一个小文件比如head -100出来的样本先跑一遍确认格式符合预期。再拿完整文件跑最后才加-i。这套流程在原始命令越复杂时越重要。特别是当你花了一小时写一条组合命令时一步到位直接作用到生产文件等回神的时候已经改完无数行了。自动化本来就快回滚却麻烦所以“跑之前先在样本上试试”是我最想强调的实操原则。6.4 慢慢建立自己的命令笔记本其实我现在写三剑客命令很少完全靠记忆。我养成了把常用命令记在小本子的习惯用文件保存就行不需要专门软件每次发现自己拼出一条既快又帅的组合就扔进去。时间长了像“日志分析模板”、“配置修改模板”、“报表输出模板”这些成套的命令积累几十条不是问题。遇到类似需求直接翻出来改改就用比临时现查快非常多。最后说点个人体会。grep、sed、awk这三个工具如果分开看每一个都有替代品——用Python写个脚本也能完成搜索、替换和统计。但把它们组合起来那种“从管道的一端输入另一端直接得到答案”的流畅感是任何脚本语言在命令行场景下都替代不了的。我见过很多开发者在服务器上遇到一个小问题下意识打开Python交互环境写二十行代码而同等需求用三剑客写一条命令就完了。不是说性能差异有多大而是你得动手打开代码编辑器的那一刻你脑子里那根“自动化处理”的弦已经断了一半。我更希望你把这套工具当成一种思维方式来学先想清楚数据长什么样、你要从中得到什么然后决定用哪一层工具——grep过滤行、sed变换文本、awk计算汇总。这种分层的思路一旦形成你会发现在命令行里处理文本从来不需要“先把文件下下来再打开”。你直接面对原始数据用几十个字符搞定一个完整的小流程这种痛快用过一次就再也回不去了。