ARTICLE DETAIL

资讯详情

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

Shell正则表达式实战:grep、sed、awk高效文本处理指南

Shell正则表达式实战:grep、sed、awk高效文本处理指南 如果你经常和Linux服务器打交道有一类需求几乎每天都躲不开从日志里过滤出某段记录、把配置文件里的某个字段批量替换、或者从文本里抽出IP、时间、金额。这些操作统称文本处理而在shell里做文本处理绕不开正则表达式。这篇文章我会把这些年用shell脚本配合正则做文本处理的思路、命令和坑一次性整理出来目标是让你看完能直接上手少走弯路。文本处理不算难难的是你把需求翻译成正则表达式的过程以及遇到各种工具行为差异时的排查思路。下面内容适合刚接触shell脚本的开发者也适合已经写了几年脚本但经常被正则细节坑到的运维和测试。1. 整体设计思路为什么文本处理离不开正则1.1 先想清楚grep、sed、awk各自负责什么很多人刚开始学shell文本处理时总想把所有工作都塞给同一个命令结果一条命令写得特别长出问题还不好排查。我的习惯是先想清楚工具分工再决定用什么命令组合。grep的核心能力是“找行”。它读入文本按行检查是否匹配某个模式然后输出命中的行。日志排障、筛选关键字、统计某个时间段内的错误记录基本都是grep的活。grep擅长只读不擅长改内容。sed的核心能力是“流式改”。它同样是逐行处理文本但能做替换、删除、插入、打印指定行等修改动作。批量修改配置文件时sed比用vim打开再手动改高效得多而且它不依赖交互终端。awk的核心能力是“结构化抽取”。它会把每一行按分隔符拆成若干字段然后按条件处理字段或者做统计。比如日志里每一行都是空格分隔的你想取第1列IP、第7列请求路径再算某类请求出现的次数这是awk的强项。在实际脚本里这三者经常接力使用。先用grep把可疑行筛出来再用awk切出关键字段最后sort、uniq做排序统计。每一个工具只做自己最擅长的事遇到问题也只排查对应环节比硬写一条几千字符的命令要好维护得多。1.2 正则方言那么多为什么我建议默认用ERE正则表达式并不是一种语法而是一族相近但细节不同的方言。对shell用户来说最常遇到三类BRE基础正则、ERE扩展正则、PCREPerl兼容正则。BRE是传统Unix工具的默认模式它的设计对现代人来说非常别扭圆括号要写成\(、\)花括号要写成\{、\}量词和?不生效要用\和\?。ERE相对符合直觉分组和量词直接写不搞多余的转义。PCRE功能最强支持\d、\w、非贪婪量词、零宽断言等但在shell里不是所有工具默认支持。我的建议是日常写正则默认使用ERE。用grep时加-E参数用sed时加-E参数awk本身就默认使用ERE风格的匹配。这样写出来的正则可读性和可维护性都远高于BRE。比如从文本里提取一个形如“192.168.x.x”的分组用BRE要写成sed s/\(192\.168\)\.\(.*\)/\1.\2/满屏反斜杠非常容易看错用ERE写成sed -E s/(192\.168)\.(.*)/\1.\2/结构就清楚很多。BRE不是不能用但它把注意力消耗在不必要的转义上。正则表达式本身已经有大量需要转义的元字符如果再套一层BRE的规则排错成本会成倍上升。除非你在处理一个只支持BRE的极简环境否则直接用ERE就好。1.3 shell里处理文本的通用三段式工作流我刚开始写shell的时候习惯把一条命令反复改、反复执行直到输出基本符合预期。后来发现效率低且容易漏边界情况现在固定用一套流程第一步构造样本输入和期望输出。先拿一个短小的测试文本手动想好匹配后应该是什么结果。正则写完后先跟期望输出比对不对再拆开分析。第二步先跑只读匹配确认命中范围。无论是grep还是sed先只做匹配和打印不要直接修改文件。grep直接执行即可sed先加-n参数配合p命令只在屏幕上看替换后的结果不落盘。第三步备份后再做批量修改。确认匹配范围没问题了再执行真正的写入操作。比如批量替换配置文件时连续执行下面几步sed -nE s/^server 192\.168\.1\.1$/server 10.0.0.1/p config.ini cp config.ini config.ini.bak sed -i.bak -E s/^server 192\.168\.1\.1$/server 10.0.0.1/ config.ini第一条命令只是把替换后的内容打印出来让你看清楚命中了哪些行第二条做备份第三条才真正写入。不要觉得麻烦这一套流程在我处理生产环境配置时救过很多次至少没有因为一个正则的边界问题导致几百台服务器的配置被改坏。2. 正则表达式语法核心细节与实操要点2.1 常用元字符速查表正则之所以看起来难是因为元字符数量多、含义又分散。只要掌握下面这张扩展正则速查表绝大多数shell文本处理场景就能应付。元字符含义例子.匹配任意单个字符a.c能匹配abc、a1c*前面的字符重复0次或多次ab*c能匹配ac、abc、abbc前面的字符重复1次或多次abc能匹配abc、abbc不能匹配ac?前面的字符出现0次或1次colou?r能匹配color、colour{m,n}前面的字符重复m到n次[0-9]{2,4}匹配2到4位数字^锚定行首^ERROR匹配行首为ERROR的行$锚定行尾done$匹配行尾为done的行[]字符集[abc]匹配a、b、c中任意一个[^]排除字符集[^abc]匹配除a、b、c外的任意字符()分组捕获(ab)匹配ab、abab|或关系cat|dog匹配cat或dog\转义\.匹配普通点号其中有个点必须反复强调点、星号、加号这些符号在正则里都有特殊含义如果只想匹配字面量必须转义。比如匹配“192.168.1.1”里的点写成192.168.1.1是不对的因为点会吞掉任意字符可能把“192x168y1z1”这种畸形内容也匹配出来。正确写法是192\.168\.1\.1。量词默认是贪婪的也就是会尽可能匹配更多字符。这一点是很多看似正确的正则在真实数据上出错的根源第2.3节会重点分析。2.2 字符类与POSIX字符类字符类用方括号表示它能匹配“集合内的任意一个字符”注意是一个字符。[0-9]匹配一位数字[a-zA-Z]匹配一个字母[0-9a-fA-F]可以匹配十六进制字符。方括号里有一些容易踩的细节。连字符-放在两个字符之间表示范围但如果放在开头或结尾就成了普通字符。想匹配“加号或减号或数字”可以写成[-]?[0-9]把减号放开头就不需要转义。另一个常见坑是方括号内想排除某种字符用[^...]但这个^只有在紧跟左方括号时才是排除含义放在其它位置就是一个普通字符。除了自己写范围正则还提供了POSIX字符类它们能处理一些容易遗漏的边界字符POSIX字符类等价含义说明[[:alnum:]]字母和数字[[:alpha:]]字母[[:digit:]]数字[[:space:]]空白字符包含空格、Tab等[[:upper:]]大写字母[[:lower:]]小写字母[[:punct:]]标点符号[[:xdigit:]]十六进制字符这里有一个新手几乎必踩的坑POSIX字符类写出来是[:digit:]但它必须再套一层方括号才能用即[[:digit:]]。原因在于[:digit:]是字符类内部的一个“类别标记”不是完整的正则表达式。如果你直接写grep -E [:digit:] file那只是匹配冒号、d、i、g等字面字符而不是数字。在高版本locale下[0-9]和[[:digit:]]绝大部分场景表现一致。但涉及到一些非ASCII字符集时字符类的行为可能受locale影响表现并不统一。写跨平台脚本时可以用[[:digit:]]替代[0-9]用[[:alpha:]]替代[a-zA-Z]可读性和兼容性都好一些。2.3 分组、反向引用、贪婪与非贪婪的实际区别圆括号在正则里有两种作用一是分组让量词作用于整个括号内的表达式二是捕获把括号内匹配到的内容保存下来后续可以用\1、\2引用。分组很直观比如(ab)能把“ab”作为一个整体重复匹配。真正有意思的是反向引用。想在一段英文里找出连续重复的单词例如“the the”这种可以写echo this is a the the apple | grep -Eo ([a-z]) \1输出the the。这里的\1引用了第一个括号捕获到的内容这种能力在数据去重、格式检查里非常实用。注意反向引用在BRE和ERE里写法基本都是\1但分组括号的写法完全不同BRE要转义ERE不需要。接下来重点说贪婪与非贪婪。默认情况下*、、{m,n}都是贪婪匹配也就是会尽量匹配更多的字符。比如下面这个例子echo tokenabctokenxyz test.txt grep -oE token.* test.txt如果你以为它会分别输出tokenabc和tokenxyz那就错了。.*是贪婪的它会在第一处匹配时一路吃到行尾实际输出是整行的tokenabctokenxyz。ERE本身不提供非贪婪量词这是一个让很多从PCRE转过来的人不适应的点。解决办法是改变思维用排除类来限定边界。对于上面的需求应该明确“token的值是不包含的连续字符”所以写成token[^]*而不是token.*grep -oE token[^]* test.txt这一次能得到tokenabc和tokenxyz两个独立匹配。这种“用排除集代替非贪婪”的做法是shell正则里最核心的实战技巧。只要你能用[^...]把终止条件表达出来基本不需要非贪婪。如果确实需要类似PCRE的非贪婪写法可以考虑grep -P配合.*?。例如从HTML里提取第一个div标签的内容grep -oP div.*?/div page.htmlgrep -P依赖PCRE库也不是所有环境都支持所以我会把它当成“最后一招”默认还是用ERE加排除类的方式解决问题。3. 三剑客实战grep、sed、awk中的正则应用3.1 grep实战过滤和提取日志先看一个实际日志场景。假设nginx的access.log里每行格式类似192.168.1.2 - - [12/Jan/2025:10:00:33 0800] GET /api/order HTTP/1.1 404 234我想快速找出所有返回4xx或5xx的请求写成一条grepgrep -E HTTP/1\.[01] [45][0-9]{2} /var/log/nginx/access.log这里的匹配逻辑是先找字面量HTTP/1.点转义[01]匹配0或1然后找引号、空格接着[45]表示状态码的第一位是4或5[0-9]{2}表示后面两位任意数字。这样既能筛出404也能筛出500、502等。再比如想从同一份日志里提取所有客户端IP来做频次统计可以执行grep -oE ([0-9]{1,3}\.){3}[0-9]{1,3} /var/log/nginx/access.log \ | sort | uniq -c | sort -rn | headgrep -o只输出匹配到的部分配合正则就能把IP从整行日志里“挖”出来。严格来说这个IP正则允许999、256这种非法数字真实日志里出现概率很低做统计足够。但如果要判断用户提交的字符串是不是合法IP那就必须用第4节里更严格的分段校验正则。grep还有一个容易忽略的点是退出状态码。grep找到匹配行时返回0找不到返回1遇到文件不存在返回2。在set -e严格模式脚本里如果某次grep没匹配到内容脚本会直接中断。想忽略这种情况就加|| true想根据结果做分支判断就直接用if。3.2 sed实战替换、删除、打印时的正则边界sed最常用的是替换命令s/正则/替换内容/。正则里用圆括号捕获内容替换内容里就能用\1、\2引用。比如从access.log里只提取日期和时间去掉其它字段sed -nE s/.*\[([0-9]{2}\/[A-Za-z]{3}\/[0-9]{4}):([0-9]{2}):([0-9]{2}).*/\1 \2 \3/p access.log | head拆开来看.*\[匹配到左中括号([0-9]{2}\/[A-Za-z]{3}\/[0-9]{4})捕获日期:后面([0-9]{2})捕获小时第三个括号捕获分钟。替换部分把它重组成“日期 小时 分钟”。加-n条配合末尾的p意思是只打印发生替换的行。这条命令我在分析按小时统计请求量时经常用。sed里替换内容中的也有特殊含义它代表本次匹配到的整段文本。比如想把所有“version1.0”整体加一层方括号
返回列表