ARTICLE DETAIL

资讯详情

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

一篇文章吃透正则表达式:常用符号、实战案例与排查技巧

一篇文章吃透正则表达式:常用符号、实战案例与排查技巧 做开发的人几乎没人能躲开正则表达式regex。不管是前端做表单校验、后端解析日志还是写脚本批量处理文本正则永远逃不掉。但每次一提到正则评论区总能看到“一学就会、一用就废”的吐槽——符号太多记不住、复杂表达式像天书、写出来还动不动就匹配不到。实际上常用的正则表达式符号翻来覆去就那么二十来个把它们分成几类逐个吃透大部分文本处理的场景就能直接上手根本不需要背教学文档里那些拗口的冷门符号。这篇我就按自己这些年踩坑总结出来的习惯把常见正则符号的底层逻辑、用法、实战示例和排查技巧一次性讲明白希望看完你能直接拿去用。1. 别急着背符号先把正则的三层逻辑理清楚1.1 为什么大多数人学正则会卡住很多人学正则失败不是笨而是学习路径反了。一上来就抱着语法大全啃看了一堆(?...)(?#...)这类冷门写法还没见到实际效果头脑就先炸了。正则不是知识记忆题它更像拼图游戏——引擎拿着你的“图案”去文本里找能拼上的地方你只需要知道图上有哪些模块、每种模块怎么组合根本不用预设自己要记住所有细节。正则表达式本质上是一套描述文本模式的微型语言。它的工作方式一句话就能讲完从左到右扫描一段文本尝试和你写的模式匹配匹配成功就算命中然后把结果交给你。那些看着吓人的符号无非是在回答几个极其朴素的问题你要匹配什么字符这些字符允许出现多少次必须出现在哪里需要把匹配结果保存成变量吗1.2 核心三件事字符、位置、结构我习惯把正则符号分成三大类去理解字符类回答“匹配哪个或哪些字符”比如\d匹配数字、[a-z]匹配小写字母。量词与锚点回答“出现多少次”“在什么位置上匹配”比如表示一次或多次、^表示行开头。分组与引用回答“怎么把一段内容打包复用”比如(...)用来捕获、\1用来反向引用。这三类功能对应了文本处理的三个核心动作查找定位特定字符、验证检查格式是否符合、提取把符合规则的内容掏出来。无论你在 JavaScript、Python、Java 还是 PHP 里写正则做的一定是这三件事中的至少一件。1.3 学正则的正确顺序先会抄再会改最后会造不推荐从语法大全开始也不推荐上来就挑战“邮箱地址万能正则”。我的建议是先学会使用已经验证过的经典表达式比如手机号、邮箱、身份证。先把它们放进代码里跑通然后一点点改里面的部分符号观察匹配结果的变化弄清楚改动的符号到底干了什么。积累十几个常用例子之后你自然会开始琢磨“我自己组合一个规则试试看”。这时候再翻语法查漏补缺基本一遍就记住了。2. 常用正则符号全拆解按功能分组逐个吃透2.1 字符组与预定义字符类从逐字匹配到一批匹配正则最基础的能力是匹配单个字符。直接写下a它就匹配文本里的 “a”。但实际中我们往往想匹配的不是一个固定字符而是一类字符——数字、字母、空白、标点中“任意满足条件的那一个”。这就是字符组Character Class出场的地方[abc]匹配 a、b、c 中的任意一个字符。[^abc]匹配除 a、b、c 之外的任意一个字符注意这里^在方括号内是“取反”的意思和行首锚点完全不同。[a-z]匹配从 a 到 z 的任意一个小写字母。[0-9]同理。字符组是万能的但写起来有点啰嗦。所以正则里内置了预定义字符类它们是字符组的高频快捷方式符号含义等价写法典型场景\d任意数字[0-9]手机号、身份证、金额\D任意非数字[^0-9]过滤掉数字\w字母、数字、下划线[a-zA-Z0-9_]变量名、用户名\W非字母数字下划线[^a-zA-Z0-9_]匹配标点符号、空格\s任意空白空格、Tab、换行[ \t\r\n\f\v]去除格式、清理文本\S任意非空白[^ \t\r\n\f\v]提取连续非空内容.除换行符外的任意字符不完全是等价关系日志提取、数据抓取这里有个新手最容易误解的点\w在 JavaScript 里默认只匹配 ASCII 的字母数字和下划线不匹配中文但你直接把[\u4e00-\u9fa5]放进字符组就能匹配中文这属于字符组最典型的扩展用法。2.2 量词决定“一个字符”重复多少次有了字符组你只能匹配一个字符。但现实中“连续出现”才是常态——手机号是 11 位数字邮箱前缀可能长度不一。量词就是用来控制字符或字符组出现次数的符号。*匹配前一个元素0 次或多次。即“有没有都行有就连着吃进来”。匹配前一个元素1 次或多次。即“必须有至少一个”。?匹配前一个元素0 次或 1 次。即“可选最多一个”。{n}精确匹配n 次。比如\d{11}就是连续 11 位数字。{n,}至少n 次。{n,m}匹配n 到 m 次。这几个量词单独理解不难难在组合使用。比如你想匹配一个“可选的负号加一到三位数字”-?\d{1,3}。它能匹配-12、345、7不会匹配-1234因为三位上限卡死了。实用经验是优先用{n,m}这种明确范围的量词尽量少用裸的*和。原因在于裸量词会引发贪婪匹配后面第 4 部分会专门讲这个坑但先记住这个原则能帮你避开至少一半的在线正则调试时间。2.3 锚点与边界锁定“位置”而不是“字符”锚点是正则里最反直觉的一类符号因为它不匹配任何可见字符只标记一个位置状态。^匹配整个字符串或行的起始位置。“必须以它开头”。$匹配整个字符串或行的结束位置。“必须以它结尾”。\b匹配单词边界即“单词字符”和“非单词字符”之间的位置。\B匹配非单词边界。为什么锚点重要写表单校验的人最懂。如果你只想验证用户输入的手机号是“1 开头的 11 位数字”却写了\d{11}那么abc12345678901def这串数据里也会被匹配到 11 位数字校验白白通过。正确写法是^1\d{10}$——用^锁死开头、$锁死结尾让整个字符串必须完完全全是手机号。这就是校验和查找的本质区别查找可以只找片段校验必须锁住整串。\b的经典场景是提取单词。比如在英文文本里\bcat\b能匹配到句子 “The cat sat” 中的 cat但不会匹配 “catalog” 里的 cat因为前者的前一位是空格、后一位是空格都是边界后者前一位也是空格t 前是空格所以有边界但catalog中后一位是字母 o不是边界整个模式就被卡掉了。2.4 分组与捕获把一段规则打包成整体括号()在正则有两大功能分组和捕获。分组层面它把多个字符组合成一个整体配合量词使用。比如(ab)能匹配ab、abab、abababab而没有括号的ab只会匹配a后面跟一堆b。这个区别一定要清楚括号把“原子”从单个字符升级成了子表达式。捕获层面括号会把匹配到的内容临时存进一个缓冲区顺序就是左括号出现的顺序第一个左括号是\1第二个是\2依此类推。这个能力在做字符串替换时极其有用。比如把2024-01-15改成15/01/2024直接用替代文本里的$2/$3/$1不同语言语法略异就能调换捕获组顺序完全不用手动拆字符串。反向引用\1则是匹配“与之前捕获完全一致”的内容。它最经典的应用是匹配重复单词(\w)\s\1可以在一段文本里找出 “hello hello” 这种连续重复的词。2.5 转义与特殊字符坑最多的部分正则里有一批自带功能的符号.*?^$[](){}\|。如果你想匹配这些字符本身的“字面意思”就必须在前面加反斜杠比如匹配一个点号要写\.匹配百分号不需要转义匹配反斜杠要写\\。跨语言的转义双坑是很多初学者崩溃的根源在 Java 和 JavaScript 等语言里反斜杠本身在字符串里也有转义功能所以正则需要双重转义。比如匹配一个数字Python 里写r\d就完事Java 里得写\\dJavaScript 里是/\\d/或者new RegExp(\\\\d)。这不是正则的规则变了是宿主语言的字符串转义先吃掉了一层反斜杠。3. 实战同台手机号、邮箱、身份证三个经典校验案例3.1 从需求到表达式手机号校验的完整推演先说需求校验一个 11 位手机号要求“1 开头 第二位 3-9 后面 9 位任意数字”。这是目前国内手机号的常见约束我以业界标准校验规则为例。逐段拆解^1必须以 1 开头。这就用掉了锚点和字面量。[3-9]第二位只能是 3 到 9 中的任意一个。这里用字符组限定范围比写[3456789]简洁也排除了 0、1、2 开头的号段。\d{9}后面跟连续 9 位数字。手机号总共 11 位前两位已定剩 9 位。$结尾锁定。拼起来就是^1[3-9]\d{9}$。这个表达式在 JavaScript、Java、PHP、Python 里写法一致区别只在字符串转义。如果只是从大段日志里提取“疑似手机号的 11 位连续数字”就不需要锚点但需要加边界避免从更长的数字串中截出 11 位。常见做法是加(?!\d)和(?!\d)这类前后断言确保匹配到的 11 位数字左右都不是数字。这个写法属于高级主题但思路就是先用简单方案跑通再针对问题补充条件。3.2 邮箱校验放弃“完美”选择“够用”网上流传的邮箱正则千奇百怪有六七十个字符的超长版本。我从从业者角度给出的建议是除非业务明确要求否则别追求 RFC 5322 标准全支持那些标准级正则可以在规范文档里当文物看日常项目用适度版本既能拦截绝大多数误输入又不会因为正则太复杂拖垮阅读和维护。我的基准版本是^[\w.%-][\w.-]\.[A-Za-z]{2,}$。拆开看^[\w.%-]开头必须是一个以上合法字符字母数字下划线、点、百分号、加号、减号对应邮箱本地部分。字面量的 符号。[\w.-]域名部分允许字母数字、点、减号。\.转义的点号匹配域名与顶级域名之间的那个点。[A-Za-z]{2,}$顶级域名至少两个字母并用$锁尾。这个表达式故意没有限制域名以字母开头还是数字开头也没有校验顶级域名的具体范围——因为把xn--这类国际化域名也放行了属于“放宽校验、减少误伤”的取舍。实际项目中如果被测试提了 bug 说“带号的邮箱也能过”你就得权衡到底是业务允许那就保留还是必须严格那就换成维度更细的校验逻辑。3.3 身份证号码校验从 15 位到 18 位正则不止匹配格式身份证号码是典型的“正则 逻辑算法”配合场景。仅靠正则只能校验格式不能验证号码真伪但格式校验本身也有不少讲究。18 位身份证的结构是6 位地址码前 2 位省、接下来 4 位市/县 8 位出生日期YYYYMMDD 3 位顺序码 1 位校验码0-9 或 X。对应的正则可以写成^\d{6}(18|19|20)?\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$。拆解这个正则的要点^\d{6}地址码是 6 位纯数字。(18|19|20)?\d{2}出身年份。身份证里如果出现 18、19、20 开头则可能是 1800、1900、2000 后的出生年。这里用括号分组和?这个可选量词是非贪婪匹配典型应用。(0[1-9]|1[0-2])月份 01-12。这里用两个分支0 开头时第二位是 1-91 开头时第二位只能是 0、1、2。(0[1-9]|[12]\d|3[01])日期 01-31。三个分支分别对应 0 开头、1/2 开头、3 开头且第二位只能是 0 或 1。\d{3}[\dXx]顺序码 3 位数字最后一位是数字或 X/xX 为罗马数字 10是校验码的一部分。光有正则还不够身份证最后一位是 ISO 7064:1983.MOD 11-2 校验码需要用前 17 位数字进行加权计算得出。很多身份证正则校验方案停在了格式校验、没做校验码验证导致随便编一个格式正确的假号码也能通过。在做强校验需求的场景比如实名认证流程的预检查时正确的做法是在正则“格式通过”之后再用一段代码计算校验码比对两者结合才是完整方案。我之前写过一版基于 JavaScript 的计算逻辑核心是“前 17 位乘加权因子求和对 11 取模映射到校验码字符表”这类逻辑与正则无关但必须和正则配合使用。3.4 从这几个案例提取通用套路从上面三个案例可以抽象出一个“三段式构造法”适用于绝大多数校验类正则先锁定边界判断是整串校验还是从文本中提取。整串校验加^...$片段提取考虑加\b或断言。再把可变部分拆成“固定前缀 主体 后缀”手机号是1 第二位取值范围 剩余位数身份证是固定长度 日期子格式。最后单独处理“可选”与“例外”比如身份证年份的(18|19|20)?邮箱顶级域名的{2,}。3.5 跨语言实现对比JavaScript、Java、Python、PHP 的写法差异同一个校验表达式在不同语言里的落地姿势不同分享一个我常用的对比视角语言正则字面量写法使用示例替换语法JavaScript/^1[3-9]\d{9}$/regex.test(str)str.replace(regex, $1)Java^1[3-9]\\d{9}$Pattern.matches(regex, str)matcher.replaceAll($1)Pythonr^1[3-9]\d{9}$re.match(regex, str)re.sub(regex, r\1, str)PHP/^1[3-9]\d{9}$/preg_match($regex, $str)preg_replace($regex, $1, $str)这份表格里有三个值得注意的点。第一Java 里\\d是因为 Java 字符串先转义第二Python 的 raw stringr...是最舒服的写法写正则强烈建议用第三替换引用捕获组的语法是$1还是\1不同语言不统一Java 和 JavaScript 用$1Python 的re.sub替换串用\1PHP 用$1。这个差异是换语言时最容易被坑的地方。4. 常见问题与排查技巧实录在线调试之外的经验沉淀4.1 正则匹配不到时先检查这三件事第一件量词范围是不是写错了。\d{11}要求连续 11 位数字如果用户输入的手机号里有空格或-分隔符比如138 1234 5678正则就直接匹配失败。这时候要么在正则里加分隔符分支要么先用字符串替换把所有分隔符去掉再校验。大部分“正则明明写对了为什么不匹配”的问题出在原始数据里有看不见的空白、换行、中文全角字符。第二件锚点用对了吗。Pattern.matches()在 Java 里是整串匹配而re.search()在 Python 里是搜索匹配找到一处就算成功。同样一个正则在不同 API 下的行为差异极大。很多从 JavaScript 转 Python 的人写re.match()以为它等于 JavaScript 的test()但re.match()只从字符串开头匹配如果字符串以换行符开头就全部凉了正确做法是先确认 API 语义再用。第三件数据里的换行符。.默认不匹配换行$在部分语言的默认模式下也只匹配整个字符串的结尾而不是每行的结尾。从日志文件里匹配一段跨越多行的文本时最常见的坑就是^和$匹配不到你以为的行。要不要开启多行模式JavaScript 的mflag、Python 的re.M取决于你要处理的数据形态。4.2 贪婪与懒惰为什么匹配出来的内容比预期多默认情况下量词是贪婪的——它会尽量吃进更多字符直到再也吃不下为止。比如文本b加粗/b 和 b另一个/b用b.*/b匹配结果不是两个独立的加粗标签而是一整段从第一个b到最后一个/b。这是因为.*先从b后的位置一路吃到文本末尾再往回退正则引擎的回溯机制找到最后一个/b作为终点。这就是贪婪匹配。解决方法是在量词后加一个?变成懒惰匹配b.*?/b。带?的写法让量词“每次只吃最少”匹配到第一个/b就停。这个?不是表示“可选”了而是修饰前面的量词为懒惰模式。注意区分ab?c中的?是修饰 b 的量词本身可选b.*?/b中的?是修饰*的匹配模式。实际项目里“匹配 HTML 标签”“提取日志中两个关键词之间的内容”几乎都绕不开贪婪和懒惰的区别。调试时如果发现匹配结果超出预期长度第一反应就应该是把贪量词换成懒惰量词试试。4.3 量词嵌套与灾难性回溯这是进阶玩家才会踩的坑但我觉得必须提前说。当你写出类似(a)这种“量词套量词”的模式去匹配一长串匹配失败的内容时正则引擎会陷入灾难性回溯Catastrophic Backtracking执行直接卡死。原理是a能匹配的字符同时被外层再重复导致每种分组可能都要尝试一遍复杂度呈指数级增长。之前调一个日志解析正则匹配一个 30 字符的错误串时卡了整整好几秒后来才发现是量词嵌套惹的祸。经验教训是不要在量词后面直接再套量词尽量利用字符组内部的重复\w而不是(\w)如果确实要表示“整体重复一次以上”也要确认每层量词的行为影响。4.4 正则调试的常用工具与正确姿势现在做正则开发比十年前好太多了在线工具和 IDE 支持已经非常成熟。我自己常用的调试流程是三步走在线可视化调试把表达式粘进支持正则可视化的工具里观察“匹配路径图”和分组捕获结构。这类工具的明显优势是能直接看到哪些字符被哪个分支吃掉了不用靠脑补。最小化失败样例如果匹配失败把输入文本缩小到 5 个字符以内再试逐步定位是哪个字符导致失败。别一口气拿 500 行日志怼上去。拆组测试从完整表达式里把括号拆开分别测每个子表达式的匹配行为。比如身份证正则先单独测^\d{6}再测日期部分最后合起来。这样问题能快速定位到具体某一段模式。另外强烈建议在代码里给每个正则加注释要么写在正则旁边的注释行里要么用带注释模式如 x 模式直接在正则里写。很多维护噩梦都是因为半年后回来看一个 50 个字符的正则不知道它在干吗然后花一上午重新推导。4.5 一些“看着别扭但很有用”的实用技巧最后分享几个实战中经常派上用场的小细节匹配中文JavaScript 用[\u4e00-\u9fa5]、Python 用[\u4e00-\u9fa5]写法相同PHP 用/[\x{4e00}-\x{9fa5}]/u加u修饰符。不同语言对 Unicode 字符类的支持度不一样建议先用短小的测试确认。忽略大小写正则引擎层面加 flag比如 JavaScript 的/abc/iPython 的re.IGNORECASE而不是把[a-zA-Z]写到字符组里占位。后者可读性差还容易漏掉特殊字符。前后断言的作用(?...)和(?...)分别表示“前面必须是”和“后面必须是”但它们本身不消耗字符。这在提取金额时特别有用(?)\d(\.\d{2})?能抓到“12.50”里的金额数字而不包含符号。常见的正则陷阱速查表我把自己常遇到的坑整理成了短表格贴出来作为参考。症状原因解决方向总是多匹配一段贪婪量词加?改懒惰明明应该有匹配却失败数据里有空格/换行/全角符号检查原始数据考虑\s和 Unicode 处理校验能通过但逻辑不允许正则只查格式不查业务规则结合代码逻辑二次校验替换得到$1字面量替换语法使用错误确认当前语言是$1还是\1正则执行超时量词嵌套或复杂回溯简化模式避免(a)类结构写到最后说点实际的个人感受。正则这门手艺上限很高但下限很低——常用的场景翻来覆去就那么几个把这几十个符号的组合关系吃透日常 90% 的文本处理都够用了。不要被“正则表达式语法大全”那种长篇文档吓退也没必要把每个冷门扩展写法都背下来遇到具体问题时知道“哦这个东西可以用分组或断言解决”再回头查语法效率远高于死记硬背。如果这篇能帮你省下点调试时间少走几个我刚入行时绕过的弯路那它就没白写。
返回列表