ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Shell正则表达式与grep/sed/awk实战:日志分析与文本处理技巧

Shell正则表达式与grep/sed/awk实战:日志分析与文本处理技巧 开头先讲个我最近实际遇到的场景。手里拿了一份三天前的nginx访问日志将近1.2GB老板让我把5xx错误请求全部筛出来再按接口路径统计一下哪些上游接口最容易超时。我当时的第一个念头是直接grep几个状态码不就行了吗结果一上手就发现正则没写对匹配出来的数据乱七八糟——4xx和5xx混在一起带时间戳的行也被误伤最后只能回头老老实实把正则表达式的规则重新捋了一遍。这也是我写这篇内容的原因。很多刚接触Shell的朋友都有类似的体验grep、sed、awk这几个工具明明天天见网上教程也一大堆但真到了自己要写匹配规则的时候要么转义符号写错要么贪婪匹配吃掉了不该吃的字符要么BRE和ERE的区别压根没搞明白。这篇东西不是教科书式地罗列语法而是从我自己处理日志、清洗文本、批量改配置文件这些真实场景出发把正则表达式在Shell环境下的应用逻辑、文本处理器的分工以及最容易踩的坑一次性说透。不管你是在做运维排查、后端日志分析还是想把手头重复性的文本处理工作自动化这篇内容应该都能帮上忙。哪怕你目前只是刚把echo hello world跑通的入门阶段跟着把示例敲一遍很多东西自己就通了。1. 正则表达式在Shell环境下的正确打开方式1.1 基础正则与扩展正则别傻傻分不清先厘清一个最基础也最容易出问题的地方Shell命令行里用的正则表达式和你之前在Python、Java或者在线正则测试工具里写的那一套表面看着像底层机制其实不一样。最常见的坑就是你在网页工具里写\d能匹配一串数字放到grep里结果啥也匹配不到——因为grep默认使用的是基础正则表达式BRE在BRE里\d并不是预定义字符类号也只是一个普通字符不代表“一次或多次”。在BRE模式下想要表示“一次或多次”得写成\想要用分组()得写成\(和\)。而grep -E才启用扩展正则表达式ERE这时候、?、|、()这些才按我们熟悉的“现代正则”规则走。再加一个grep -P可以启用PCREPerl兼容正则支持\d、\w这类写法但这是GNU grep的扩展能力跨平台到BSD/macOS上的时候不一定默认可用。我个人的习惯是写命令的时候优先用grep -E或者sed -E明确切换到ERE模式这样语义最清晰也减少了转义符号满天飞的问题。用一个小例子说明——# BRE模式下号是普通字符写成\才是“一个或多个” grep ro\t test.txt # ERE模式下号直接就是“一个或多个” grep -E rot test.txt二者效果一致但看着舒服程度天差地别。所以当你在别人的脚本里看到一堆披着反斜杠的正则时先别急着抄确认一下对方用的是哪种模式。1.2 常用语法速查字符类、量词、锚点和分组正则这东西语法条目能列出一长串但实际日常运维和开发中用得最频繁的其实就那二十来个。我把它们按用途归了类做成一个可以直接翻阅的速查表。类别语法ERE含义示例字符类[abc]匹配任意一个列出的字符gr[ae]y可匹配gray或grey字符类[^abc]匹配不在列表中的任意字符[^0-9]匹配非数字字符类[a-z]匹配范围内的字符[A-Za-z_]匹配字母或下划线预定义.匹配除换行外的任意字符a.c可匹配abc、a1c预定义[[:digit:]]POSIX字符类等价于[0-9][[:digit:]]匹配数字预定义[[:space:]]匹配空白字符空格、Tab等[[:space:]]匹配空格量词*前一个字符出现0次或多次ab*c可匹配ac、abc、abbc量词前一个字符出现1次或多次abc可匹配abc、abbc量词?前一个字符出现0次或1次ab?c可匹配ac、abc量词{n,m}前一个字符出现n到m次[0-9]{2,4}匹配2到4位数字锚点^匹配行首^ERROR匹配以ERROR开头的行锚点$匹配行尾done$匹配以done结尾的行分组(...)将多个字符组合为一个整体(ab)匹配ab、abab交替ab匹配a或b转义\将特殊字符还原为字面量\.匹配点号本身这里多说一句[[:digit:]]这种POSIX字符类。它在老牌Unix工具里是标准能力兼容性最好比\d可靠得多。在Shell环境下写脚本我建议优先用它代替\d尤其是要同时跑在Linux和macOS上的脚本。1.3 文本过滤的本质正则不是用来“写”的而是用来“读”的正则表达式在各种语言里都像一种迷你编程语言但在Shell环境下它更像一把“筛子”的齿间距——本身没什么独立意义关键看配在哪个工具上、筛什么流、留什么行。理解这一点比死记硬背语法更重要。理论上一个正则表达式的匹配过程就是一个有穷状态机在字符串上的遍历。用大白话说就是从文本的某个位置开始按照你写的规则一个字符一个字符地去试能从头走到尾就算匹配成功如果中间某一步对不上就回退重来。Shell里的grep、sed、awk做的事情本质上都是在不同的数据流处理阶段反复调用这套匹配引擎。举个生活化的类比。正则里的.就像“任意一个扑克牌都能顶上的万能牌”*就像“这副牌里某一种花色你可以随便出几张甚至不出”^和$则是“这一局必须从第一张开始数”和“必须数到最后一张为止”。把这些牌理清楚了匹配规则自然就好写了。2. 文本处理三剑客grep、sed、awk的分工与配合2.1 grep不只是“找出来”还能按语境统计很多人对grep的认知停留在“搜索包含关键字的行”但它的能力远不止如此。常用参数里-c统计匹配行数-v反向匹配-o只输出匹配的部分而不是整行-A和-B分别输出匹配行之后和之前的若干行上下文-r递归搜索目录-i忽略大小写-l只列出包含匹配内容的文件名。我最常用的一个组合是grep -E -o搭配正则提取日志中的特定片段。比如从下面这行JSON格式的日志里提取user_id的值echo {event:login,user_id:12345,ip:10.2.3.4,status:ok} \ | grep -oE user_id:[0-9] \ | cut -d: -f2 12345这里[0-9]在ERE模式下表示连续的1到多位数字-o只把匹配到的部分吐出来再交给cut按冒号切分拿到值。这套组合拳在处理非结构化文本时几乎天天用。2.2 sed流式编辑里的“查找替换”与“定点手术”sed的核心能力是逐行读取、逐行处理、逐行输出所以它特别适合“按规则批量修改文件内容”这种场景。最常用的是替换命令s/旧内容/新内容/默认只替换每行第一个匹配加g后缀则替换该行所有匹配。# 把配置文件里的旧IP换成新IP-i表示直接修改文件 sed -i s|192\.168\.1\.10|10.0.0.8|g /etc/app/config.ini这里有个小细节分隔符我用的是|而不是默认的/。因为要匹配的IP地址里本身就带斜杠风格的句点结构如果还用/做分隔符正则里转义点号的反斜杠和分隔符混在一起可读性会变差。分隔符可以换成任意字符在地址和替换文本中包含特殊符号时非常实用。除了替换sed还能按行号或模式做定点删除和插入。比如删除一个配置块中所有注释行sed -i /^#/d /etc/app/config.ini这行的意思是匹配所有以#开头的行然后执行d删除操作。注意这里的^#用的是BRE模式因为是行首加普通字符不需要转义。实际工作中我经常用sed -n 10,20p只打印某一段范围内的内容来快速定位问题避免整个文件刷屏。2.3 awk字段切分与条件计算让文本处理有“逻辑”如果说grep是筛子sed是手术刀那awk就是一套小型的“数据处理流水线”。它默认按空白字符把每一行切分成多个字段$1、$2……分别对应第1列、第2列$0代表整行。配合BEGIN和END块可以做累加、统计、格式化输出。看一个典型的日志统计场景。nginx的access log默认格式大致是192.168.1.100 - - [10/Oct/2024:13:55:36 0800] GET /api/user/info HTTP/1.1 200 5321如果想统计每个请求路径被访问了多少次awk {print $7} access.log | sort | uniq -c | sort -rn | head -20先取第7列也就是请求路径sort排序uniq -c去重并计数sort -rn按数字逆序排最后取前20。这一串命令组合起来比写一整段Python脚本简洁太多。awk里还能做条件过滤比如只统计状态码大于等于500的行awk $9 500 {print $1, $7, $9} access.log$9对应状态码那一列条件成立时打印出IP、路径和状态码。这种“先切列、再判断、再处理”的模式是awk区别于前两者的核心价值——它让文本处理不止于“筛选”还能做真正的逻辑计算。2.4 三者的分工口诀grep负责找sed负责改awk负责算这三个工具虽然都能处理正则但侧重点完全不同。grep是查找工具职责是“把符合条件的行/片段从文本流里挑出来”sed是流编辑器职责是“在不打开整个文件的情况下按规则修改内容”awk是报表工具职责是“对结构化文本进行字段提取、统计和计算”。用一句话总结先用grep缩小范围再用sed做模式替换最后用awk做字段统计。这条流水线能覆盖90%以上的日常文本处理需求。我还会在管道链中尽量让每一步只做一件简单的事这样出了问题更容易定位是哪一步不对。3. 实战环节日志分析、字段提取与批量文本清洗3.1 场景一从访问日志中统计5xx错误分布现在回到开头那个1.2GB日志的问题。第一步先用grep把5xx相关的行筛出来。注意状态码可能出现在行内位置不固定更稳妥的做法是匹配HTTP/1.1 5xx这种靠近行尾的上下文grep -E HTTP/1\.[01] 5[0-9][0-9] access.log这里的5[0-9][0-9]精确匹配500到599不会错把500开头的URL路径混进来。如果只想统计5xx的总数grep -cE HTTP/1\.[01] 5[0-9][0-9] access.log接下来按接口路径做分布统计需要先知道路径是第几列。对nginx默认日志来说请求行是第7列但这列包含请求方法、路径和协议版本如果只想保留路径部分可以用awk进一步切分grep -E HTTP/1\.[01] 5[0-9][0-9] access.log \ | awk {method$6; path$7; code$9; if (path ~ /\?/) {split(path, a, ?); patha[1]} print code, path} \ | sort | uniq -c | sort -rn | head -30这里split(path, a, ?)的作用是把带查询参数的URL去掉参数部分只保留路径。实际处理这种大文件时我会先用head -100抽样验证字段列数对不对再跑全量不然1GB文件跑一半发现第7列根本不是路径就白等了。3.2 场景二从混合文本中提取关键字段很多系统的原始日志不是规整的JSON或CSV而是杂糅了各种信息的自然语言风格文本。比如[2024-10-10 13:55:36] [INFO] [order-service] order_id7890123456, user_id8842, amount199.50, statuspaid我需要把所有order_id提取出来生成订单清单。用sed配合-E和-o提取grep order_id app.log \ | sed -E s/.*order_id([0-9]).*/\1/ \ order_ids.txt这条命令的原理是sed的替换操作会匹配整行利用.*吞掉order_id前后的所有内容再用括号([0-9])捕获订单号替换时通过\1反向引用只保留捕获的那部分。把这一行拿出来单独看echo [INFO] order_id7890123456, user_id8842, amount199.50, statuspaid \ | sed -E s/.*order_id([0-9]).*/\1/ 7890123456这个用法几乎是文本清洗的万能套路。同类场景还有从URL日志里提取域名、从错误堆栈里提取异常类型、从数据库备份里提取表名等等。3.3 场景三批量重命名与配置文件批量修改有一批文件名是这样的report_20241010_final.txt、report_20241011_final.txt想统一把_final去掉。用shell的for循环配sed处理字符串再mv重命名for f in report_*_final.txt; do newname$(echo $f | sed -E s/_final\.txt$/.txt/) mv $f $newname done这里用到了$(...)命令替换把sed处理后的结果赋给变量newname。注意变量名和文件名最好都加上双引号防止文件名里有空格时被拆成多个参数。配置文件批量修改的场景也类似比如把某个服务所有环境配置里的数据库密码占位符__DB_PASSWORD__替换成真实密码sed -i s/__DB_PASSWORD__/MyNewPass123!/g config/*.env这里!在双引号内会触发shell历史扩展所以要小心。更安全的写法是单引号包正则里面有!也不会有问题。踩过坑之后我对替换文本里含特殊字符的情况一律优先考虑单引号。3.4 场景四用awk做累加统计与报表输出再回到日志假如要统计所有成功请求的总流量并且按小时分组输出。nginx日志最后一列通常是响应字节数第4列是时间戳[10/Oct/2024:13:55:36 0800]。用awk实现awk { hoursubstr($4, 14, 2); bytes$10; if (bytes ~ /^[0-9]$/) sum[hour]bytes; } END { for (h in sum) printf hour%s, total_bytes%d\n, h, sum[h]; } access.log | sort这里substr($4, 14, 2)从[10/Oct/2024:13:55:36 0800]中截取小时部分第14位开始两位。sum[hour]bytes是awk的关联数组累加最后在END块里统一输出。bytes ~ /^[0-9]$/的作用是排除掉-这样的占位符避免把非数字累加进去。注意sort放在最后因为for遍历关联数组的顺序不固定必须靠管道排一下。4. 常见坑与排查技巧实录4.1 转义地狱多一层shell解析就多一层反斜杠Shell环境下的正则难点不在正则本身而在“正则还要被shell先解释一遍”。用双引号包正则时$、!、反引号都可能被shell先处理。所以我的原则是正则表达式优先用单引号包裹让shell完全不做任何变量展开和命令替换把字符串原样传给grep或sed。只有在确实需要shell变量参与正则拼接时才用双引号。# 错误示范双引号内的$会被shell展开 grep status$code app.log # 正确示范先用单引号匹配再通过变量拼接时单独处理 code500 grep status${code} app.log另外在sed的替换文本里遇到字符要格外小心它在替换文本中代表“整个匹配到的内容”。如果想替换成字面量需要写成\。这种细节不踩一次坑很容易忽略。4.2 贪婪匹配为什么正则“吃”掉了不该吃的内容正则默认是贪婪的.*会尽可能多地匹配字符。比如从titleShell正则/title中提取标题如果写echo titleShell正则/title | grep -oE title.*/title结果会把整个titleShell正则/title都匹配出来因为.*吞掉了中间的文本直到最后一个/title才停下来。想要最短匹配在ERE模式下没有.*?这种懒加载写法常规做法是用排除法把标签本身排除在字符类外echo titleShell正则/title | grep -oE title[^]*/title[^]*的意思是“匹配任意个不是的字符”这样匹配到第一个/title之前就会停下。这个方法在解析HTML标签、提取JSON值、处理CSV字段时非常实用基层原理就是限制“任意字符”的范围比试图让它“别贪心”更可靠。4.3 性能陷阱大文件处理前先想想顺序处理大日志时性能问题很容易出现。我的经验是三条原则。第一尽量让数据在管道前段变小。能用grep先筛掉一半行就不要把所有行都交给复杂的awk处理。grep基于C的快速匹配性能远高于后续逻辑处理。第二避免无谓的cat。cat file | grep xxx完全可以写成grep xxx file少一个进程少一次管道复制对GB级别文件的性能影响肉眼可见。第三注意awk里正则的频繁调用。如果每条记录都要做多个正则匹配考虑能否先用字符串函数index()或substr()做粗筛再用正则做精筛。在日志分析中字段值往往是规整的纯字符串函数往往比正则快一个数量级。4.4 常见问题速查表问题现象可能原因快速排查/修复方法grep \d匹配不到数字\d是PCRE语法BRE/ERE中不识别改用[0-9]或[[:digit:]]或加-P参数sed里号不生效sed默认BRE是字面量用sed -E启用ERE替换文本里的变成了匹配内容在替换文本中代表“整个匹配”写成\表示字面量.*匹配结果太长贪婪匹配用[^...]排除法实现最短匹配变量在双引号正则里被意外展开shell优先解析$静态正则用单引号动态拼接确认变量名无误awk的$9打印为空字段数少于9个先awk {print NF}看字段总数再调整索引大文件grep很慢模式过宽或管道前段数据量太大先精确锚定行首/行尾再逐步缩小范围递归搜索时大量无关文件被扫到grep -r没限制文件类型加--include*.log限定扩展名4.5 调试技巧先用小样本验证再上全量这是我想额外强调的一点。正则写完后不要直接全量跑大文件先取前几行用head -5或者sed -n 1,20p做样本把命令的输出结果和预期对比一下。比如我经常先跑head -100 access.log | grep -E HTTP/1\.[01] 5[0-9][0-9] | awk {print $7} | head这一步能快速发现列数对不对、过滤条件是否符合预期。确认无误后再把head -100换成完整文件跑全量。这种做法看着多了一步实际上节省了大量的试错时间。另一个技巧是用echo构造一个模拟输入直接验证正则本身的行为尤其是sed的替换规则。多试几组边界情况空字段、负数、超长文本比自己面对一份真实大日志盲猜要高效得多。5. 工具选型与进阶建议5.1 什么时候不需要Shell正则用别的方式更快正则和文本处理器再强也有它们不太擅长的场景。比如需要解析结构复杂的嵌套JSON或XML应该用jq、yq或Python的json模块硬用正则去抠嵌套结构很容易出错。表格数据的复杂计算用awk可能写得很别扭用python3 -c或pandas更顺手。需要跨行匹配的场景比如匹配从div到对应/div的完整块grep和sed默认都是逐行处理很难用正则完成这时用perl -0777或者Python读取整个文件会更合适。Shell正则的适用边界是“不要求复杂结构解析的流式文本处理”。一旦发现自己为了一个正则的边界情况反复调了半小时还不过说明该换工具了。5.2 进阶方向把正则能力迁移到其他语言正则表达式是跨语言通用的底层技能Shell里学会的匹配思维可以平滑迁移到Python、JavaScript、Go等几乎所有主流语言。到了Python里你会接触到re.search、re.findall、re.sub这些API正则语法从ERE变成了更完整的PCRE风格支持\d、\w、非贪婪.*?、前瞻后顾等高级特性。但核心的匹配思路、字符类设计、贪婪与性能意识都是在Shell里锻炼出来的。我的建议是先把grep -E、sed -E、awk这三板斧练熟能解决80%以上日常问题之后再去学Python正则的高级特性。因为Shell环境反馈最快一条命令一个结果试错成本极低用来打正则基础是最好的环境。5.3 写给新手的实操路线如果是刚接触Shell的朋友我建议按下面的路径走一遍每一步都动手敲不要只是读准备一个示例文件粘贴几行nginx日志或系统日志。用grep完成三种最基本的操作按关键字筛选、按行首匹配、按行尾匹配。用sed做一次简单的替换再练习把某一行删除、把某一行前插入新内容。用awk实现对某一列求和、按条件过滤、用printf格式化输出。组合使用管道把前三步串起来处理一个完整的小任务比如统计日志中某个状态码的数量。最后把常用正则写成小抄放在手边遇到不确定的语法就查一下用多了自然就记住了。这个路径不涉及复杂环境配置只要有Linux或macOS的终端或者Windows上的WSL就可以开始。练习的数据可以直接从/var/log/下的系统日志里取也可以自己用seq和printf造一批模拟数据。6. 结语这层能力越早掌握后面越省力写了这么多想对正在读这篇文章的你说的最后一句话是正则表达式和文本处理器看着是Shell里的“小工具”实则是效率提升的杠杆点。别人花一小时手工改配置文件你用sed一条命令三秒搞定别人用Excel处理日志统计你一条awk管道直接输出报表。这种差距不是天赋差距而是工具掌握程度的差距。我个人在实际操作中的体会是正则这个东西入门门槛其实不高难的是“用得准”。多写多试把错误当作排查训练慢慢就会形成一种“看到文本就想到匹配规则”的直觉。而一旦形成了这种直觉再去看Python、Go、JavaScript项目里的字符串处理逻辑你会发现很多地方都能触类旁通。最后再分享一个小技巧保存一份自己的“正则常用片段库”。每次在工作中写出一条好用的正则命令就顺手记到一个markdown文件里按用途分类IP提取、时间戳解析、URL清洗、JSON字段提取等。时间久了这就是你处理文本问题时最快的查表工具比任何教程都管用。
返回列表