ARTICLE DETAIL

资讯详情

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

Shell文本处理三剑客:正则表达式与grep、sed、awk实战

Shell文本处理三剑客:正则表达式与grep、sed、awk实战 1. 为什么说正则表达式是Shell文本处理的“入场券”1.1 从一次日志排障聊起去年冬天我在线上排查一个接口响应变慢的问题手头只有一份两百多兆的访问日志。我想从里面把超时超过5秒的请求全部捞出来再把请求的来源IP和耗时字段整理成一张表。如果用编辑器打开再搜基本等于加班到天亮。最后我用了三行命令前后不到一分钟就拿到了结果。第一行是grep过滤出超时记录第二行是awk提取关键列第三行把结果按耗时排序。那一刻我真正理解了为什么Linux老手总说“不会正则和文本处理器等于没进Shell的门”。其实很多人刚学Shell的时候都觉得cd、ls、for循环就算入门了。但真正让Shell发挥出威力的是对文本流的处理能力。服务器上几乎所有状态都藏在日志和配置文件里而这些文件无一例外都是文本。想快速从里面找到、抽取、修改内容靠的就是正则表达式配合grep、sed、awk这一套组合拳。1.2 正则表达式在这里到底扮演什么角色正则表达式不是Shell的三剑客之一但它是三剑客共同的“弹药”。grep负责匹配sed负责按地址处理awk负责按列拆解——它们背后都依赖同一个东西你怎么描述“我要找的内容”。正则表达式解决的就是让机器理解“你想要哪几行、哪些字段”。我带的几个新人经常问明明可以用cat加less去查为什么非要记正则答案是“可重复”。你在终端里手翻一遍只是满足这一次需求但把一条正则写进脚本里下次日志一更新你就能自动得到同样的统计结果。运维的价值在于自动化自动化的第一步就是精确描述目标文本。所以我一直认为在Shell编程这条路上正则表达式不是选学内容而是和for循环、变量、管道一样的基础设施。理解了这一点后面所有文本处理的学习都会顺畅很多。2. 在Shell里写正则先要过“转义和方言”这两关2.1 单引号、双引号与正则转义的相爱相杀这是新手踩得最密集的一个坑。同样一条正则写在单引号里和双引号里解析结果可能是两回事。Shell做了一件事它先把命令行里的字符串交给某个程序之前会对双引号内的内容做变量展开、命令替换等处理而单引号内部则完全是原样。举个例子在grep里匹配包含美元符号的行# 单引号内是正则$在正则里表示“行尾” grep error$ app.log # 双引号内$会被Shell当变量开头如果环境里没有该变量就变成了空字符串 grep error$ app.log第二条命令实际会被解析成grep error app.log匹配的是所有带“error”的行完全违背了本意。更麻烦的是反斜杠。比如你想匹配一个句号.在正则里要写成\.。但如果用双引号包起来Shell在把参数传给grep之前可能先把\.处理成.正则就变成了匹配任意字符。我给自己定了一条规矩正则表达式一律用单引号包裹除非内部真的需要Shell变量参与拼装。这样能省掉八成转义问题。比如匹配带引号的文本时就会简单直观grep \status\: 200 access.log2.2 POSIX字符类与Perl转义符的区别很多从Python或Java转过来的朋友习惯写\d表示数字、\w表示单词字符。但在Linux自带的grep里默认使用的是正则库并不是Perl兼容的语法。grep -E支持的是“扩展正则表达式”也不认识\d。正确做法是用POSIX字符类# 匹配所有含手机号的行11位数字 grep -E [0-9]{11} user.log # 用POSIX写法语义更清楚 grep -E [[:digit:]]{11} user.log[[:digit:]]这种写法看起来很啰嗦但它能保证在任何locale下都正常工作。类似的还有[[:alpha:]]、[[:space:]]、[[:alnum:]]。在awk里情况更特殊awk自带的正则引擎支持类似Perl的转义不同版本有差异gawk里\w、\y都可辨认这导致很多人在grep和awk之间来回切换时因为“语法方言”不同而踩坑。我自己记忆的方法是grep和sed使用同一套正则体系awk自己玩自己的。如果你需要与Perl兼容的正则grep有一条额外参数-P但这条参数在macOS原装的BSD grep里是不支持的跨环境时要谨慎使用。2.3 分组和反向引用给重复的模式一个“变量”分组用()它除了用于调整优先级还能把匹配到的内容存下来后面通过反向引用\1、\2取用。在Shell文本处理中这个能力经常被用来做“提取”或“格式重排”。最典型的场景是sed替换里的分组。比如把姓名:张三 电话:13800000000这种格式改成CSV里的张三,13800000000echo 姓名:张三 电话:13800000000 | \ sed -E s/姓名:(\S) 电话:(\S)/\1,\2/这里(\S)匹配连续的非空白字符把中文字段名后面的内容整体存进\1和\2然后通过替换模板重排出需要的顺序。注意我用的是sed -E它的作用相当于grep的-E启用扩展正则表达式这样括号不需要写\(和\)。反向引用在awk里同样存在但使用时要格外小心awk里的\1只在某些特定函数如match()、gensub()中才生效不像sed那样直接写在s///里。所以如果你在awk里用sub()做替换很多老版本不支持反向引用最好用gsub()配合捕获或直接用index()/substr()处理。这个区别非常细微但排障的时候能卡死你半小时。3. grep、sed、awk三把刀的正确打开方式3.1 grep -E过滤和提取的“手术刀”grep的本职是“过滤行长”。它不修改文件内容只负责把符合条件的行挑出来。我用的最多的几个参数参数作用示例-E启用扩展正则grep -E 404-o只输出匹配的部分不输出整行grep -oE 1[0-9]{10} user.log-c统计匹配行数grep -c ERROR app.log-v反向匹配输出不含该模式的行grep -v ^# nginx.conf-A 2/-B 2同时输出匹配行的后两行/前两行grep -A 2 FATAL error.loggrep -o是一个特别适合提取字段的命令。比如想从一整行JSON里抽出所有“order_id”的取值不需要写awk直接grep -oE order_id:[0-9] data.json它会返回所有匹配到的零散片段。虽然多加点逻辑可以格式化成纯数字但在“只要能肉眼确认”的排障阶段这种方式最快、最直观。3.2 sed按地址做替换和删除的“流编辑器”sed和grep最大的不同是它可以把匹配到的行做替换、删除、插入而且能原地修改文件。常写脚本的人离不开sed做配置变更。sed的基本模型是“按行读取 → 做操作 → 输出”。常见的操作指令有s/old/new/g、d删除、p打印行。注意默认情况下sed不打印未做操作的行但如果显式用-n配合p就能实现“只输出指定行”的效果# 只打印第10到20行 sed -n 10,20p file.txt地址部分支持正则比如修改从[server]开始到结束的块sed -i /\[server\]/,/^\[/ s/port3306/port3307/ my.cnf这里的,表示“从第一个模式到第二个模式之间的所有行”是区间寻址的经典用法。-i表示直接修改文件。我强烈建议在首次使用-i时带上备份后缀sed -i.bak s/old/new/g config.conf命令会在修改前生成一个config.conf.bak备份文件。一旦替换结果不对还能飞快回滚。这个习惯在批量改生产配置时尤其重要别问我怎么知道的。3.3 awk面向列的“报表生成器”awk算是三兄弟里最难学也最强大的一个。它默认以空白字符空格/制表符把一行拆成列$1、$2代表第一列、第二列$0代表整行。NF代表当前行总共有多少列NR代表当前是第几行。举个例子看/etc/passwd里用户的UID和登录Shellawk -F: {print $1, $3, $7} /etc/passwd-F:把字段分隔符改成了冒号随后打印用户名、UID和Shell路径。awk的价值不止于拆列它还能做统计。比如快速看nginx日志中各个状态码的数量awk {count[$9]} END {for (code in count) print code, count[code]} access.log这条命令把第九列状态码作为数组的下标做累加处理完所有行之后在END块里遍历数组输出。这种写法在Shell里几乎等同于一行“迷你报表程序”比写Python再去导包利落得多。3.4 三者的配合管道化思维的起点刚开始学的时候很多人会纠结“一个问题到底该用grep还是awk还是sed”。我的判断顺序很简单只要“找出来看看”用grep找出来之后还要改格式、替换文字用sed要拆分列、做统计、按条件计算用awk。复杂任务通常是三者在管道里接力完成的。比如统计今天每个来源IP的访问次数并且只保留前5名grep 2023-12-01 access.log | awk {print $1} | sort | uniq -c | sort -rn | head -5这段命令的每一步都只做一件事输出像水流一样被后面接着处理。这比写一个几十行的Python脚本更快、更贴合Shell的哲学小而精的工具串成链条。理解了这个“管道链”思维你就不会在Shell里硬造一个大而全的怪物了。4. 实战从Nginx日志里提取状态码和客户端IP4.1 场景设定和数据格式假设我手上有一个标准Nginx日志文件access.log每行的格式是192.168.1.10 - - [20/Jan/2025:10:15:32 0800] POST /api/order HTTP/1.1 500 512 0.278 203.0.113.5 - - [20/Jan/2025:10:16:01 0800] GET /api/user HTTP/1.1 404 153 0.045字段之间用空格隔开双引号里是请求行后面的500是状态码0.278是响应耗时秒。现在需求是找出上午10点到11点之间的所有非200请求并且统计这些请求的来源IP Top10还要求把IP的第4段打码后输出比如192.168.1.*避免泄露内网结构。4.2 第一步用grep筛出目标时间范围时间字段在方括号里我们可以用正则锚定“小时”的位置。10:15:32这样的格式可以用10:[0-5][0-9]:来匹配。整条命令grep -E \[20/Jan/2025:10:[0-5][0-9]: access.log | grep -vE HTTP/1\.1 200 两条grep用管道串联第一个筛出10点时段第二个反向匹配状态码不是200的行。这里我把“200”前后带上了空格和引号尾巴就是为了避免误匹配“2000”这种数字。正则里的“边界感”非常重要很多统计错乱都是因为模式写得太宽泛。4.3 第二步用awk提取IP并统计Top10时间过滤已经完成下一步就是把源IP第一列取出来做次数统计。这里直接用awk一步到位grep -E \[20/Jan/2025:10:[0-5][0-9]: access.log \ | grep -vE HTTP/1\.1 200 \ | awk {ip[$1]} END {for (addr in ip) print ip[addr], addr} \ | sort -rn | head -10sor如果没有预先按数字排序sort -rn会正确按第一列数字从大到小排列。head -10截取前十条。整个链条的输出来就是128 192.168.1.10 56 203.0.113.5 ...4.4 第三步用sed做IP脱敏和格式美化如果接下来要把这些IP导出给外部同事看就需要脱敏。IP地址是“点分十进制”把最后一段统一改成*用sed最方便... | sed -E s/[0-9]\.[0-9]\.[0-9]\.[0-9]/\1.\2.\3.*/诶这里有个问题sed的基础正则里如果要用分组得写\(\)然后反向引用\1。为了简化我建议直接用sed -E然后分组写成([0-9]\.){3}这种形式。再结合前面的统计结果脱敏命令可以是awk {ip[$1]} END {for (addr in ip) print ip[addr], addr} access.log \ | sed -E s/^([0-9] )([0-9])\.([0-9])\.([0-9])\.([0-9])$/\1\2.\3.\4.*/这里把前面的输出又调整了一下第一列是次数第二列是IP正则捕获前四个数字最后一段被替换为星号。这样就既保留了访问量信息又隐藏了主机号。4.5 完整的脚本串起来把上面的步骤封装成一个Shell脚本以后直接复用#!/usr/bin/env bash log${1:-access.log} hour${2:-10} echo 统计 $log 中上午 ${hour}:00-${hour}:59 的非200请求按IP排名 grep -E \[.*${hour}:[0-5][0-9]: $log \ | grep -vE HTTP/1\.1 200 \ | awk {ip[$1]} END {for (addr in ip) print ip[addr], addr} \ | sort -rn \ | head -10 \ | sed -E s/^([0-9] )([0-9])\.([0-9])\.([0-9])\.([0-9])$/\1\2.\3.\4.*/把它保存成top_ip.sh加上执行权限就能跑。这段脚本能复用一个大前提日志格式是默认的Nginx combined格式。如果你自己改过log_format列的顺序就要相应调整。5. 新手最容易翻车的三个细节5.1 贪婪匹配与最小匹配正则表达式中*和默认是贪婪的会匹配尽可能长的内容。这带来的典型问题常见于HTML、JSON这类“有开始标记和结束标记”的文本。比如我想从日志里提取两个双引号之间的HTTP请求行grep -oE .* access.log这一条命令的意图可能是匹配POST /api/order HTTP/1.1但贪婪模式下.*会把从第一个双引号到本行最后一个双引号之间的所有内容都吃掉结果变成了POST /api/order HTTP/1.1 500 512 0.278 GET...等等整行都被匹配掉。解决方法是尽可能用字符类缩小范围。HTTP请求行里不包含双引号所以可以写成grep -oE [^]*这里的[^]*表示“非引号字符连续出现任意次”这样匹配到第二个双引号立刻停下。正则里没有“非贪婪”这个钱包按钮可用时[^特定字符]通常是更稳的解法。道理很简单你描述的不是“任意内容”而是“除了闭合符之外的任意内容”。5.2 多行内容怎么处理grep、sed、awk默认都是一行一行处理文本。碰到日志跨行、配置项被换行拆开的场景新手就会蒙圈。比如XML配置里一个标签的内容可能分成多行description 这是第一行 这是第二行 /description如果用grep匹配description只能拿到第一行和最后一行中间正文是拿不到的。此时应该在sed里用区间寻址sed -n /description/,/\/description/p config.xml这样就打印了从开始标签到闭合标签之间的所有行。如果还要把两行合并成一行可以配合sed的N命令或者用awk的RS分隔符。比如awk把整个文件当作一条记录来读再用RS处理多行日志是我用过最顺手的方案awk BEGIN{RS\n\n} /Traceback/ {print $0} error.log把段落分隔符设成了空行这样每段错误日志就成了一个个整体只要某段包含“Traceback”整段都会被打印出来。在排查Python异常栈时这招比grep好用太多。5.3 原地修改的坑-i的兼容性与备份sed -i几乎是日常改配置的默认姿势但它跨平台有坑。Linux自带的GNU sed里-i不需要参数macOS原本的BSD sed里-i必须紧跟一个参数作为备份后缀哪怕你想不备份也得写成-i 。很多人在macOS上直接跑sed -i s/a/b/g file结果报错或者提示“sed: 1: file: undefined label”。另一方面哪怕在Linux上我也建议养成带备份后缀的习惯。你可以先在不加-i的情况下跑一遍让结果输出到屏幕确认无误后再加-i.bak。这一步能避免整个文件被替换坏的高血压时刻。6. 把正则和文本处理器用好的个人手记用了这么多年Shell我最大的体会是正则不是背出来的是“打”出来的。每遇到一个陌生文本格式我都先做三件事第一用grep -oE把最小匹配片段打出来看看第二用sed -n s///p做替换预览第三用awk把列结构打印出来甚至先打印NF和NR来观察结构。如果感觉表达式越来越长、越来越绕就该停下来拆管道。一行巨型正则往往让人花一小时也调不出来但拆成三步每步只处理一个子问题十分钟就搞定。比如把“提取IP 统计次数 排序”拆成三段debug起来非常清晰。另外我习惯把经常要用的文本处理片段写进一个~/.bashrc函数里比如extract-ip()、count-status()。别小看这个动作积累半年之后你处理日志的速度会比同事快一个量级。最后提醒一句正则表达式是工具不是炫技场。能让别人一眼看懂的命令永远比“一行流花活”更重要。写完后加一句注释说明当初为什么这么写三个月后的你会感谢现在的你。
返回列表