
1. 正则表达式到底在解决什么问题我第一次接触正则表达式是为了处理一份几千行的日志文件。那里面密密麻麻记录了用户访问记录需求也简单——把所有手机号提取出来存到Excel里给运营做回访。当时我用的还是最原始的办法一条一条复制、粘贴折腾到凌晨三点还剩小半眼睛都快瞎了。后来组里的老哥看不下去丢了一行东西给我1[3-9]\d{9}我把它粘进UltraEdit的查找框勾上正则选项一键全选——几千个手机号整整齐齐列在那里。那一刻的感受就是这玩意简直是魔法。所以我后来一直在想正则表达式为什么让初学者觉得难不是难在语法多而是难在不知道它到底解决什么问题。如果只当成查找工具来学你会觉得它不过是高级点的CtrlF不值得花那么多功夫。但一旦你意识到正则实际上是在给计算机描述一种文本模式而不是在搜索某个具体字符串整个思路就会打开。简单说正则表达式是一个模式描述语言。它不关心你具体要找张三还是李四它关心的是我要找的是一串以1开头、第二位是3到9的数字、后面再接9位数字这样一类字符串。这个能力在日常开发里有多常用几乎所有主流语言——Python、Java、JavaScript、Go、PHP、SQL Server后面我会专门讲这个——都内置或通过第三方库支持正则。我见过很多人事到临头才去网上搜邮箱正则、身份证正则复制粘贴回来发现跟自己的场景对不上然后抱怨正则不可靠。实际上正则是一套通用的字符匹配语言一旦理解底层的几个概念这些现成的正则你不但看得懂还能自己改。这篇文章我不会把它写成一份枯燥的语法手册。我的计划是先拆掉正则最核心的几个积木然后用Python的re模块真刀真枪跑一遍接着单独解决一个热搜词里提到的实际问题——13位手机号的正则到底怎么写最后聊聊SQL Server里怎么用正则。在这个过程里我会把我踩过的坑、复盘出来的经验一并放进去。这里要说明一点正则表达式的规则非常多我当年光是查表就查了三天但真正干活时经常用到的、高频的那部分其实非常少。你不需要学完所有特性才动手掌握下面这张核心清单80%的日常需求就已经能覆盖了。元字符含义生活化类比.匹配任意单个字符换行符除外扑克牌里的万能牌\d匹配任意数字0-9专门认数字的探测器\w匹配字母、数字、下划线识别单词字符的扫描仪\s匹配空白字符空格、Tab、换行等扫地机器人专清空白[]匹配方括号中任意一个字符限定范围的安检门[^]匹配不在方括号中的任意字符安检门的反向模式*前一个字符重复0次或多次允许出现但不强求前一个字符重复1次或多次至少得出现一次?前一个字符重复0次或1次可选可不选{n,m}前一个字符重复n到m次精确掐定次数^匹配字符串开头行首哨兵$匹配字符串结尾行尾哨兵|或逻辑选了A就不选B()分组把一组字符打包处理快递打包盒这张表里的东西我们接下来一个一个用实际例子打通。2. 从零拆解核心元字符不是背是用2.1 字符匹配的底层逻辑先分清单个字符再谈组合正则看起来复杂但拆到最底层就两个动作匹配单个字符和控制重复次数。你看到的任何复杂正则本质都是这两类动作的嵌套组合。先聊聊匹配单个字符。这个好理解你写一个字母a它就匹配文本里的字母a。但正则真正强大的地方在于它能匹配一类字符而不是一个字符。\d匹配任意一个数字字符——注意是一个。如果你想匹配一个数字后面再跟一个数字就得写\d\d如果想匹配十一个数字写十一个\d当然可以但正常人不会这么干。所以才需要后面的量词来控制重复次数。这里有个初学者最容易绕晕的点\d到底匹配一个还是一串答案是一个。它本身只能匹配一个字符但是配合量词或*就能匹配连续的一串数字。你把\d理解成一块砖量词理解成要几块砖问题就清晰了。再来看字符组[]。它解决的是另一个问题如果我要匹配的字符不是任意数字这么规整的类别而是a、b、c、d四个字母中的任意一个该怎么办用字符组[abcd]这表示匹配a、b、c、d中的任意一个字符。字符组内部还支持范围简写[a-z] # 匹配任意小写字母 [A-Z] # 匹配任意大写字母 [0-9] # 等价于\d [a-zA-Z0-9_] # 等价于\w字符组里还有个反向操作[^...]。^放在字符组开头表示除了这些字符以外的任意字符。比如[^0-9]匹配任意非数字字符。这个在实际处理脏数据时极其好用——比如你要把文本里的非数字字符全部剔除留着数字做下一步处理。2.2 量词与贪婪为什么正则有时候会多吃多占量词是正则里另一个核心。刚才说了*表示0次或多次表示1次或多次?表示0次或1次{n,m}表示n到m次。它们都修饰前一个字符。但这里有个天坑我当年在这个坑里挣扎了很久——贪婪匹配。所谓贪婪就是正则表达式在默认情况下会尽可能多地匹配字符。举个例子文本是b加粗/b和u下划线/u我用.*去匹配直觉告诉我应该匹配到b这个标签对吧但实际上匹配到的是b加粗/b和u下划线/u整个一长串。为什么因为.能匹配任何字符*又贪婪地吞掉了所有内容直到最后一个才停下来。这就好比一个人吃自助餐规则是从第一个菜夹到最后一个菜他不会只夹第一盘而是从头吃到尾。正则在量词后默认就是这种从头吃到尾的状态。解决办法是在量词后面加一个?变成非贪婪模式.*?这样它会匹配到第一个就停下来得到b、/b、u、/u四个标签。我遇到过不止一次同事说我的正则怎么把整行都匹配走了十有八九就是贪婪匹配惹的祸。在你写任何带.*或.的正则时脑子里都要自动闪过一个问题这个量词在前面会不会吞太多确认要匹配最短结果就果断加?。2.3 分组与捕获把打包盒用明白分组()在正则里承担着三个角色优先级控制、整体量词修饰、捕获内容。先说优先级。正则里的|符号表示或。如果你写abc|def它匹配的是abc或者def。但如果你写ab|cdef意思就变成匹配ab或者cdef。看到区别了吗|的作用范围是它左右的整个片段不是单个字符。如果你想表达ab后面跟的是c或d得加分组ab(c|d)这样就把a、b、以及c或d拼在了一起。再说整体量词。(ab)表示ab这一组可以重复1次或多次。没有分组的话ab只会让b重复a只有一次。第三是捕获。当你用分组把一部分内容包起来正则引擎会自动把分组匹配到的内容记下来。在Python的re模块里.group(1)就能取到第一个分组的内容.group(2)取第二个。这个能力在做提取类需求时是杀手锏——比如从日志里同时提取时间和手机号两个括号一组各取各的。有意思的是如果你只想用分组控制优先级、但不想让内容被捕获占内存、影响性能可以写成(?:...)。这种非捕获分组我在写复杂正则时几乎必用因为能有效减少捕获组数量让代码更清晰。3. Python里的正则实操从re模块说起3.1 四个最常用的re函数先分清再动手Python的正则功能集中在re模块里。文档很长但高频函数就四个match、search、findall、sub。我自己的经验是先分清这四者的边界否则很容易调了半天API结果发现用错了函数。re.match()从字符串的起始位置尝试匹配。注意是起始位置不是全字符串任意位置。如果字符串开头不符合模式立刻返回None。它用来做格式校验特别合适——比如判断一个字符串是不是以数字开头。re.search()从整个字符串中搜索第一个符合模式的子串。不要求从开头匹配只要某处能命中就行。它返回一个匹配对象里面带有分组信息。re.findall()把所有符合模式的子串以列表形式返回。这是提取数据时的主力函数。一个容易踩的坑是如果你的正则有分组findall()返回的不是整个匹配而是每个分组的内容组成的元组。这个行为跟很多人直觉完全不同。比如import re text 手机号是13812345678备用号13987654321 result re.findall(r1[3-9](\d{9}), text) print(result) # 输出 [3812345678, 3987654321]我只想要完整手机号但因为加了分组结果被截断了。解决办法有两个第一用非捕获分组(?:...)第二改成finditer()。re.sub()用来做替换。它跟字符串的.replace()的区别在于前者支持模式匹配后者只支持字面量替换。比如你想把文本里所有的手机号中间四位打码text 手机号是13812345678 masked re.sub(r(1[3-9]\d)\d{4}(\d{4}), r\1****\2, text) print(masked) # 输出 手机号是138****5678这里的\1和\2引用的是两个分组的内容。第一组(1[3-9]\d)占了前三位第二组(\d{4})占最后四位中间四位用星号替换。这个操作处理敏感信息脱敏时非常常见。3.2 写正则前想想要不要加r前缀Python里写正则我一直建议统一加r前缀比如r\d。这个r代表原始字符串意思是不做转义处理。如果不加r写\d时Python会把\d当普通转义处理可能直接报错或者行为异常。举一个很实际的问题匹配一个反斜杠\。正则里想匹配字面量的反斜杠本身就要写成\\。在Python字符串里如果你不加r写\\\\才是能匹配一个反斜杠的正则——四个反斜杠极其反人类。但如果加了r只需要写r\\。谁优谁劣一目了然。这个细节我觉得是Python正则初学者最容易栽跟头的地方而且报错信息还不直观往往是结果匹配不上不是程序崩溃。3.3 Python正则实例从日志里批量提取信息光说不练假把式。我们拿一个实际需求串一遍假设有一份服务器日志每一行长这样2024-05-20 14:23:11 ERROR 用户13912345678 登录失败 2024-05-20 14:25:37 INFO 用户13811112222 支付成功 2024-05-20 14:30:02 WARN 用户13733334444 接口超时需求是把出错的用户手机号和对应时间全部提取出来。我先写一个能匹配整行关键信息的正则import re log 2024-05-20 14:23:11 ERROR 用户13912345678 登录失败 2024-05-20 14:25:37 INFO 用户13811112222 支付成功 2024-05-20 14:30:02 WARN 用户13733334444 接口超时 pattern re.compile(r(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ERROR 用户(1[3-9]\d{9})) matches pattern.findall(log) for time_str, phone in matches: print(f时间: {time_str}, 手机号: {phone})输出时间: 2024-05-20 14:23:11, 手机号: 13912345678这里我用re.compile()先把模式编译成对象好处是同一模式多次匹配时性能更好代码也更清晰。时间部分用\d{4}-\d{2}-\d{2}匹配日期空格加\d{2}:\d{2}:\d{2}匹配具体时间。中间的ERROR是字面量文本原样写在正则里就行。这个例子里隐藏了一个经验写正则永远先想清楚你要保留哪部分、跳过哪部分。需要保留的用括号包住变成捕获组不需要的用字面量文本或非捕获匹配跳过。4. 热门需求拆解13位手机号码的正则到底怎么写4.1 从最简单的版本开始推演热搜词里有一条是13位数字手机号码正则表达式怎么写。这个问题挺有意思因为它牵出一个思维过程到底是任意13位数字还是以合理号段开头的13位数字最朴素的写法\d{13}这个能匹配任意13位连续数字。听起来没啥问题但实际用起来漏洞不小。举个例子一段文本里有一段身份证号18位或者订单号或者卡号只要是连续13位数字都会被它误伤。正则匹配不考虑这是不是手机号它只看字符模式。所以在真实项目里几乎不会用这种裸奔版。手机号正则必须考虑三件事号段范围、边界约束、是否允许前缀。4.2 手机号正则的演进从号段约束到边界匹配先说号段。国内手机号第一位固定是1第二位目前实际在用的有3-9所以前两位可以写成1[3-9]。接下来还有9位数字用\d{9}表示。组合起来1[3-9]\d{9}这个就是我在文章开头用到的版本。它比\d{13}精度高很多至少不会把一堆以2、4、5开头的随机数字当手机号。但这还没完。假设你的文本里有一段是手机号是1134567890123注意这是14位数字。用1[3-9]\d{9}去匹配正则引擎会从第2位开始取照样能把134567890123中的后11位匹配成一个手机号。这算不算错取决于你的场景。如果你只是做模糊提取可能无所谓如果你要做严格校验就麻烦了。严格校验需要用到边界匹配。在Python正则里\b匹配单词边界^和$匹配整个字符串的开始和结束。做完整字段校验时一般这么写^1[3-9]\d{9}$^和$把整个模式钉死在字符串两端确保整个字符串就是一个手机号不多不少。做文本内提取时可以在前后加\b\b1[3-9]\d{9}\b\b的作用是防止手机号前后紧挨着其他数字或字母。这里又是一个容易忽视的细节\b匹配的是位置不是字符所以它不会消耗任何字符。4.3 更完整的版本考虑86和区号热搜词只有13位数字手机号码这么简单但我建议一步到位把可能的前缀也考虑进去。现在很多系统里存的手机号带国际区号86或0086你要做匹配时可以先考虑兼容(?:\?86)?1[3-9]\d{9}拆开看(?:\?86)?表示可选的86前缀86中的在正则里是特殊字符所以要写成\转义?表示可以出现也可以不出现整个括号外的?表示86整体可有可无。后面再接标准的手机号主体。这条正则能屈能伸匹配13812345678没问题匹配8613812345678也没问题。但要注意加了前缀后边界匹配要重新设计因为\b在号旁边可能会出现偏差。对于大部分场景我建议匹配前先把文本里的86、0086等前缀统一清洗掉再用干净的11位正则去处理。省事也避免出错。我再分享一个实战中总结的经验不要写一个万能正则去应对所有格式。键盘输入的手机号千奇百怪有人加空格138 1234 5678有人加横线138-1234-5678与其写一个长到看不懂的正则去兼容所有情况不如先预处理——去掉空格、横线、括号统一成纯数字串再匹配。这条思路可以推广到几乎所有数据清洗场景先规范化再匹配。5. SQL Server中的正则别被不支持骗了热搜词里出现sql server 正则表达式说明很多人想在数据库层面直接做模式匹配。我直接说结论SQL Server原生内置的T-SQL语言并没有像MySQL那样直接提供REGEXP运算符。但这不代表你在SQL Server里用不了正则只是需要换一条路。5.1 用LIKE模拟简单场景但别指望它干重活SQL Server里最接近正则是LIKE运算符配合通配符%匹配任意长度字符、_匹配单个字符、[abc]匹配字符集合中的任意一个、[^abc]匹配不在集合中的任意字符。这套语法能解决一部分半正则需求。比如你要找出所有phone字段以13、15、18开头的记录SELECT * FROM users WHERE phone LIKE 1[358]%这条SQL里1[358]相当于正则的1[358]%相当于.*。说实话在简单的场景下够用了。但LIKE的局限性也特别明显。它不支持量词{n,m}不支持分组和或逻辑|不支持贪婪/非贪婪控制。你想在SQL Server里匹配13位数字这个要求用LIKE写起来相当别扭-- 判断字段是否纯数字且长度13位 WHERE phone NOT LIKE %[^0-9]% AND LEN(phone) 13第一行用的是双否定技巧[^0-9]匹配任何非数字字符%包住它表示只要存在任何非数字字符就算命中NOT LIKE过滤掉这些剩下的就全是纯数字了。再加长度判断勉强实现了\d{13}的效果。但这种用技巧硬凑的方式可读性差维护起来也痛苦。如果你只有一个两条查询需求忍忍也就过去了如果你要在存储过程、视图、报表里反复用我建议用下面这两种更彻底的办法。5.2 CLR正则和第三方函数SQL Server里的正则方案SQL Server可以通过CLR集成Common Language Runtime来加载.NET的正则库。简单说就是写一段C#代码编译成DLL部署到SQL Server里之后就能像调用普通函数一样使用正则。这个方案功能最完整但需要开CLR权限还要有Visual Studio环境编译DLL对很多非.NET背景的开发来说门槛偏高我个人不太推荐在日常项目里轻易引入。另一个思路是使用SQL Server 2016数据库引擎的TRANSLATE、STRING_AGG等功能组合出一些字符处理方案但它们本质上还是字符串函数不是正则。坦白说如果你的需求比较复杂、又必须要在数据库层解决我见过更多团队的做法是把数据捞到应用层用Python或Java处理而不是在数据库里硬扛。这样既绕开了SQL Server正则能力弱的短板又利用了应用层成熟的正则库代码可读性和可维护性也更好。毕竟查询性能和数据量也值得考虑正则表达式在数据库里跑大表很容易拖垮性能。5.3 SQL Server正则应用场景举例数据质量检查我们假设你确实需要在SQL Server层面做一点正则式的数据校验——比如检查身份证号是否合法、邮箱格式是否正常。虽然T-SQL不支持完整正则但可以用嵌套函数实现一部分。比如检查邮箱WHERE email LIKE %__% AND email NOT LIKE %[^a-zA-Z0-9._-]%第一行确认有至少一个字符 至少一个字符的结构第二行排除包含非法字符的邮箱。这种方案能挡住明显不合法的数据但挡不住格式完全合规的假邮箱。所以说来说去数据库里做正则校验永远是在可用性和完整性之间做取舍。真遇到严格校验需求数据量不大时我还是建议用Python的re模块在应用层做全量扫描清晰可靠还方便写测试用例。6. 写正则常见的坑与排查链路我踩过的雷6.1 坑一\转义引发的连环爆炸几乎每一个正则初学者都会在转义上栽跟头。正则里的\d、\w这些反斜杠本身就是转义字符而很多语言里字符串本身也把\当作转义符。双重转义叠加很容易混乱。举个实际例子。我想匹配一个IP地址比如192.168.1.1。正则写法可以是\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}注意IP地址里的点.在正则里是任意字符要匹配字面量的点必须写成\.。这个反斜杠是正则层面的转义。到了Python字符串里你还要考虑Python的转义规则。如果用普通字符串写\d{1,3}\.\d{1,3}...Python的字符串解析器会先处理\.但因为在Python字符串里\.并不是一个合法的转义序列Python会保留它虽然版本不同有差异最终传给正则引擎的仍是\.恰好能工作。但如果遇到\dPython会尝试解析\d由于\d同样不是合法的Python转义序列Python也保留为\d所以经常歪打正着能跑。真正出问题的是匹配反斜杠本身的时候你想匹配一个Windows路径C:\Users\name正则需要写成C:\\Users\\name正则层面到了Python字符串里每一个\\又要再翻倍最后写成C:\\\\Users\\\\name。这种代码你说它可读性好鬼都不信。所以再次强调Python里写正则r前缀是救命的path_pattern rC:\\Users\\name别在这个地方省事省下的几秒钟会让你在调试时多花几个小时。6.2 坑二^和$在多行模式下的行为变化默认情况下^匹配整个字符串的开头$匹配整个字符串的结尾。但在处理多行文本时你可能希望^和$也能匹配每一行的行首和行尾。Python里可以通过re.MULTILINE标志简写re.M开启。我踩过的坑是提取日志时想找所有以ERROR开头的行但忘了加re.M结果只匹配到整个日志文件里第一个出现在开头的ERROR后面几十条全漏了。这跟你用findall还是search没关系是^的语义没搞对pattern re.compile(r^ERROR, re.MULTILINE)开了多行模式后^会匹配每个换行符之后的位置findall()就能把所有以ERROR开头的行都捞出来了。还有一个细节Windows和Linux的换行符不一样$在默认模式下可能匹配不到行尾的\r。处理Windows文本时正则写\r?$比直接写$更稳妥。6.3 坑三分组捕获在findall里的变魔术前面提到过findall()遇到带分组的正则时返回的是分组内容而不是整个匹配。这是个让我记忆深刻的坑因为它完全不报错结果却跟预期完全不一样。想想这个场景你想提取文本里的时间戳正则写(\d{4})-(\d{2})-(\d{2})然后用了findall()。你期待返回类似2024-05-20的字符串列表结果返回的是(2024, 05, 20)这样的元组列表——每一个时间都拆散了。第一次遇到这种结果我还以为是数据问题排查了好久才发现是findall的机制。解决这个问题我通常用三种手段根据场景选把不需要捕获的分组改成非捕获分组(?:...)使用finditer()遍历每一个匹配对象再通过.group()取想要的字段逐个search()循环匹配。我个人的经验是finditer()往往是最稳的。它返回迭代器每个元素是匹配对象既能拿到整个匹配也能取分组还不会像findall那样自作主张地改变返回结构。6.4 排查链路的完整复盘一个真实的调错过程讲一个我最近帮同事排查的小案例完整还原一下思路。需求是从一批订单备注里筛选出含有13位手机号的订单。同事写了这个正则跑在Python里r\d{13}跑出来的结果里有大量误匹配比如订单号202405201234567890119位被截取出了中间13位还有一串类似1234567890123的随机数字也被当成手机号。我们先从现象入手误匹配的全是不是手机号的长数字串。根据这个观察第一件事就是收紧模式——手机号不是任意13位数字而是1开头、第二位在3-9之间所以改成1[3-9]\d{9}。跑完一部分还是有问题因为有些订单号恰好也以1开头、第二位也是3。接下来我们意识到需要用边界约束。加\br\b1[3-9]\d{9}\b这一下误匹配少了很多但还有个别订单号正好是13位数字、以1开头、且前后是空格或标点被误伤了。到这里我们评估了一下完全消除误匹配靠正则几乎不可能因为订单号本来就可能是任意数字组合。于是我们把方案调整为先用正则粗筛拿到候选集合再用一个二次校验函数比如检查号段是否真实存在、是否满足校验规则做精确过滤。这其实是很多生产级系统的通用做法正则负责找个大概业务逻辑负责精确裁决。这个复盘过程想说明一个道理正则不是魔法它只是一个模式的描述工具。它擅长的是按形状找东西而不是按语义判断东西。遇到语义判断的需求别硬用正则搭配合适的业务逻辑才是正道。6.5 实战中的性能优化技巧正则写得好不好性能差距可以非常夸张。我曾经处理过一份几GB的日志一个没优化好的正则需要跑将近一分钟优化到最终版本之后几秒钟就出结果。性能优化有三个最重要的切入点。第一个是避免灾难性回溯。像(a)这种嵌套量词量词套量词很容易让正则引擎在匹配失败时做指数级的回溯尝试直接卡死。看到嵌套量词时要么简化要么用原子组或占位量词如果语言支持。第二个是能用字符组就不用.。\d比.快[0-9]比.*快因为字符组的匹配范围明确引擎可以快速判断是否命中不用做大量回溯试探。第三个是预编译正则。在Python里用re.compile()把模式编译成对象下次调用直接复用能省去每次匹配时的编译开销。如果你是循环里对几千条记录做匹配这个优化性价比很高。7. 最后的经验分享写到这里正则表达式的核心知识基本都过了一遍。我从一个差点被几千行日志逼疯的初学者到现在能顺手写出一段正则来处理各种文本清洗需求中间走过不少弯路。如果让我总结一句最想对初学者说的话别把正则当成一门需要学完才能用的学科把它当成一个查字典就能干活的工具。我自己的学习路径是先强记那十几个高频元字符一张表就够然后立刻拿去解决实际问题。遇到写不出来的就去搜索引擎搜类似案例参考别人的写法再一行行拆解理解。用不了两周你会发现大多数正则你看一眼就能猜个八九不离十。再分享一个工作习惯凡是稍微复杂一点的正则我都要求自己写完后留下一段注释解释这个正则是干嘛的、每一部分匹配的是什么。这不是为了别人是为了三个月后的自己。正则这玩意是出了名的写了就忘当初没注释后来看着自己写的一长串乱码真想穿越回去给自己一拳。最后一个建议是善用正则测试工具。在线工具有很多比如RegEx101、regexr左边写正则、右边写测试文本匹配结果实时高亮还能逐段解释每个元字符的含义。调试复杂正则是用这类工具比反复在代码里打印输出快得多。配合Python里的re模块边写边测正则表达式这门初探就算真正入门了。接下来你要做的就是去找一个真实数据场景上手试一把。