正则这东西几乎是每个写代码的人迟早都要碰的“硬骨头”。你可能是因为表单校验、日志提取、爬虫解析或者只是想把一段乱七八糟的文本里藏着的数字和符号抠出来才点开了这篇内容。先说清楚这篇文章不是教材是我自己多年踩坑、翻文档、写工具积累下来的常用正则表达式清单和实操思路覆盖JavaScript、Python、C#这几个主流语言里最常见的用法。不管你是刚接触正则的新手还是已经写了几年代码但每次遇到复杂匹配都要重新翻手册的老手这篇都值得收藏备用。我在工作中最常见的需求无非这么几类校验用户输入邮箱、手机号、身份证这类格式、从日志或网页中提取关键信息数字、日期、URL、特定标记后的字符串、以及清洗脏数据去掉空白、去重、替换敏感词。如果你也有类似的场景往下看就对了。开场不废话直接把最核心的匹配原理、二十来个高频正则、以及一个完整的“提取中间数字和#号后字符串”实战案例拆给你看。1. 正则表达式的底层逻辑先看破再写不慌1.1 正则到底是怎么匹配字符串的很多初学者一上来就背语法结果写出来的正则又长又错。我自己觉得先弄明白正则引擎是怎么工作的比背一百条规则都管用。简单说正则表达式的核心就是“按模式逐个字符地扫描目标字符串”引擎从字符串的某个位置开始尝试用你的模式去匹配匹配不上就回溯换一个位置直到找到符合条件的子串或者明确失败。这个过程中最关键的概念叫“回溯”。举个例子你写\d去匹配abc123def引擎会先让\d尽量多吃数字贪婪吃到123之后发现后面是d不是数字匹配结束拿到123。但如果你的模式是\d3那么\d一开始会把123全吃掉然后发现后面没有3于是它会把最后一位3吐出来让模式中的字面量3去匹配最终匹配结果是123的23不对让我重说实际上是从字符串开头位置开始\d 会尝试尽可能多匹配吃掉 123而后面没有字符无法匹配字面 3于是回溯让 \d 匹配12字面 3 匹配原字符串第三个字符3最终整个模式匹配到123。这就是回溯。理解回溯之后你再去看“贪婪匹配”和“懒惰匹配”的区别就一目了然。.*是贪婪它会匹配到字符串最后一个符合条件的字符为止.*?是懒惰它匹配到第一个符合条件的字符就停。很多人写正则提取“中间内容”失败十有八九是没用对这两个。1.2 元字符、字符类、分组和断言先把地基打牢正则的语法看着多但真正高频的也就下面几张牌元字符就是.*?^$|这些有特殊含义的符号字符类是方括号[0-9][a-z][\u4e00-\u9fa5]分组用圆括号(...)它除了把表达式打包还能“捕获”匹配到的内容方便你提取断言则是(?...)和(?...)这类它们只占位置不占字符用来做前后条件的约束。我见过太多人一上来就想写一个“万能正则校验邮箱”其实拆开来看很简单用户名部分允许字母、数字以及_-.然后一个再然后域名部分由一到多段至少两个字符的字母数字组成最后是一个点加顶级域名。写成^[\w.-][\w-](\.[\w-])$就已经能覆盖绝大多数场景了。关键在于你得清楚每一段匹配的是什么字符、允许重复多少次、起止位置在哪里而不是凭空抄一段看不懂的东西。2. 20个高频正则表达式大全直接抄进代码就能用2.1 数据校验类锚定符号是灵魂这一节我直接放干货把我最常用的20个正则整理成表。每个都可以在当前主流语言里直接用但要注意校验类场景必须用^和$把两端锚定否则你校验“邮箱”的时候abcqq.comxxx也会被当成合法值因为引擎默认做的是“包含匹配”而不是“完全匹配”。用途正则表达式匹配示例邮箱^[\w.-][\w-](\.[\w-])$test.nametagabc.com.cn中国大陆手机号^1[3-9]\d{9}$13800138000身份证号^\d{17}[\dXx]$110101199001011234IPv4地址^((25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(25[0-5]|2[0-4]\d|1?\d?\d)$192.168.1.100URL^https?://[\w-](\.[\w-])[^\s]*$https://example.com/page?id1日期年-月-日^\d{4}[-/]\d{1,2}[-/]\d{1,2}$2024-06-18纯数字正整数^[1-9]\d*$9527金额两位小数^\d(\.\d{1,2})?$299.50用户名4-16位字母数字下划线^[a-zA-Z0-9_]{4,16}$dev_007密码强度含大小写字母和数字^(?.*\d)(?.*[a-z])(?.*[A-Z]).{8,}$Abc12345邮政编码^\d{6}$200000QQ号^[1-9]\d{4,10}$123456789中文姓名^[\u4e00-\u9fa5]{2,4}$张三HEX颜色值^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$#A1B2C3时间HH:mm:ss^([01]?\d|2[0-3]):[0-5]?\d:[0-5]?\d$23:59:59车牌号简化版^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$京A12345提取所有数字\d从 abc123 里提取 123匹配空白符\s匹配空格、Tab、换行首尾去空白^\s\s$匹配重复单词\b(\w)\s\1\b匹配 is is这张表里前几个是校验类后面的偏提取和清理类。实际使用时像IP地址这种“看上去很长很好用”的正则其实性能并不算好。如果你的环境允许比如Python的re模块不支持但第三方regex库支持更推荐用\b(?:\d{1,3}\.){3}\d{1,3}\b先粗提取再用代码判断每个数字是否在0到255之间代码清晰且不容易错。2.2 校验类正则为啥必须加锚点很多初学者写手机号校验上来就是1[3-9]\d{9}然后拿去测试我的号码是13800138000请惠存发现也能匹配成功于是以为正则没问题。实际上这埋着一个大坑正则默认找的是“包含匹配”。如果你的原意是“用户输入的一个字段必须完整等于手机号”那不加^和$就会放过一堆脏数据比如13800138000extra这种。我习惯把所有需要完整校验的表达式都统一加上^(?:... )$注意如果表达式里本身有|要用非捕获分组(?:...)包住整个结构否则^只作用于第一个分支很容易出现“只校验了一半”的诡异bug。另外像身份证号最后一位可能是X要写成[\dXx]同时要注意是否允许小写x不同业务要求不一样这个细节特别容易被忽略。2.3 文本提取类正则贪婪和懒惰必须分清提取类正则的目的不是判断“是不是”而是“抠出来”。这里()捕获组是核心。比如从order_no: A10086X提取10086就可以写A(\d)X通过反向引用或API拿到第一个捕获组的内容。捕获组从1开始编号$1或\1可以引用。最常见的提取需求之一是“提取中间的数字”。比如商品编号格式是SKU-88231-2024你想拿中间那段88231就写-(\d)-注意\d会尽量多吃数字直到遇到下一个连字符为止刚好取到中间段。如果你的分隔符在结尾没有后边界比如SKU-88231那-(\d)$会直接匹配到末尾也够用。但假如你要提取price299.50, discount19.90中的价格用price(\d\.\d{2})就可以了没必要写全局匹配。如果同一段文本里出现多个价格就在支持全局匹配的语言里加上g标志或用findall配合捕获组一次性拿全。3. 实操现场提取中间数字和#符号后的字符串3.1 需求拆解为了演示一条完整路径我从热搜词里挑一个具体场景从一段含“中间数字”和#符号的文本里把目标内容精确提取出来。假设有一段聊天日志订单号PO-88468-2024付款金额 299.50优惠券 #SUMMER50实付 #249.50客服 #小美需求是拿到PO-88468-2024中间的88468同时提取所有#后面的字符串包括SUMMER50、249.50、小美。这个场景非常典型几乎涵盖了数字提取、符号边界、中文匹配、全局提取所有核心技巧。第一步先拆“中间数字”PO-(\d)-2024这个正则可以取到88468。但更通用一点的做法是只要某个数字前后都是-就可以用-(\d)-提取适合不知道前缀和后缀具体值的情况。第二步拆#后面的内容这里的关键是“边界”。#后面可能是英文、数字、小数点、中文。如果写#(\w)\w默认匹配字母数字下划线在Python里还匹配中文但C#和JavaScript的\w通常不含中文。所以稳妥写法是#([^\s])意思是#后跟一个或多个非空白字符这样英文、数字、中文全都能抓到而且不会跨行。如果想排除标点可以再细化字符类但通常[^\s]已经够用。3.2 用三种语言写出完整实现JavaScript版本const text 订单号PO-88468-2024付款金额 299.50优惠券 #SUMMER50实付 #249.50客服 #小美; // 提取中间数字 const orderMatch text.match(/PO-(\d)-2024/); if (orderMatch) { console.log(orderMatch[1]); // 88468 } // 提取所有#号后的字符串 const tags text.match(/#([^\s。])/g); console.log(tags); // 这里要注意中文标点[^\s] 不会排除全角逗号所以用 [^\s。] 更好我特意在JS例子里加了一个细节[^\s]不会排除全角逗号所以如果#SUMMER50后面跟中文逗号#SUMMER50会被一起抠出来。你可以用match(/#([^\s。])/g)或者匹配完再对结果做一次replace(/[。]$/, )具体看你的数据定。实际项目里我更喜欢用matchAll配合捕获组const pattern /#([^\s。])/g; const matches [...text.matchAll(pattern)]; const cleanTags matches.map(m m[1]); console.log(cleanTags); // [SUMMER50, 249.50, 小美]Python版本import re text 订单号PO-88468-2024付款金额 299.50优惠券 #SUMMER50实付 #249.50客服 #小美 order_match re.search(rPO-(\d)-2024, text) order_id order_match.group(1) if order_match else None print(order_id) # 88468 tags re.findall(r#([^\s。]), text) print(tags) # [SUMMER50, 249.50, 小美]Python里re.findall一旦正则中含捕获组返回的就是捕获组的内容而不是整个匹配这个行为经常让新手困惑但用对了非常省事。注意我写成了原始字符串r...这是Python正则的推荐写法可以省去一堆反斜杠转义。C#版本using System; using System.Text.RegularExpressions; var text 订单号PO-88468-2024付款金额 299.50优惠券 #SUMMER50实付 #249.50客服 #小美; var orderMatch Regex.Match(text, PO-(\d)-2024); if (orderMatch.Success) { Console.WriteLine(orderMatch.Groups[1].Value); // 88468 } var tagMatches Regex.Matches(text, #([^\s。])); foreach (Match m in tagMatches) { Console.WriteLine(m.Groups[1].Value); } // 输出 SUMMER50、249.50、小美C#的Regex.Matches会返回所有匹配项配合命名捕获组(?tag[^\s。])可读性更强后面m.Groups[tag].Value就能直接取值。C#里建议在正则前加这样\d这类转义字符不会被C#编译器吃掉一层写起来和看手册一样直观。3.3 为什么.*?有时比(\d)更稳回到提取“中间数字”你可能在网上看过一些教程写#(.*?)#或者-\d-\d这种复杂写法。我的建议是能用字符类精确匹配时尽量别依赖泛化的.*?。拿-(\d)-来说\d只匹配数字它天然限定了“中间”的范围不会误抓到-abc-这种非数字段。而如果写-(.*?)-中间内容是什么都能匹配你必须再花代码去判断它是不是数字多一步不说还容易漏掉边界情况。当然.*?也绝不是一无是处。当你确实需要“开头到末尾之间的一切”时.*?是最简洁的工具。比如提取title我的博客/title里的标题写title(.*?)/title就非常合适因为标题本身可能是中文、英文、数字任意组合。这里有个细节如果用贪婪的.*同样可以匹配到标题但它可能吞掉后面的内容。比如title我的/titlep博客/ptitle首页/title贪婪版本会从第一个title一直吞到最后一个/title中间全部当成标题。而懒惰版本.*?会在遇到第一个/title时停下正好取得“我的”。这算是我工作中遇到频率最高的一个坑。4. 不同语言的平台差异写错平台代码全白搭4.1 JavaScript、Python、C# 正则语法对比同样一个正则在不同语言里行为可能天差地别这绝对不是危言耸听。我列个表把这几个最常踩的语言差异摆出来差异点JavaScriptPythonC#全局匹配使用g标志match才能拿到全部匹配使用re.findall/re.finditerRegex.Matches天然返回全部匹配命名为捕获组(?name...)或(?name...)新版支持(?Pname...)(?name...)分组引用替换时用$1替换时用\1或\g1替换时用$1匹配首行^匹配字符串开头或行首多行模式默认只匹配字符串开头多行模式用re.M默认同Python多行模式用RegexOptions.Multiline\d匹配范围匹配 ASCII 数字 0-9默认匹配 Unicode 数字比如阿拉伯语数字默认匹配 Unicode 数字可用RegexOptions.ECMAScript限定为 ASCII中文匹配\w不含中文需用[\u4e00-\u9fa5]\w默认包含中文取决于unicode支持\w不含中文需用\p{IsCJKUnifiedIdeographs}或字符类这里我想强调一个最容易翻车的点\d的范围。Python 的re模块默认把\d当成 Unicode 数字意味着如果文本里出现了阿拉伯语数字٠١٢或其他 Unicode 数字字符\d也能匹配上。对国际化的项目这可能是特性但对国内很多只处理大陆手机号的业务来说就是bug。解决方案是写[0-9]代替\d或者用re.ASCII标志。C# 里则可以用RegexOptions.ECMAScript把\d拉回 ASCII 行为。JavaScript 没有这个问题因为它一直只认 0-9。这些细节不注意你会在生产环境收到“明明写了手机号校验阿拉伯数字也通过了”的诡异工单。4.2 灾难性回溯一个正则拖垮整个服务正则写不好不会只是输出不对还可能让CPU直接飙满接口超时这就是“灾难性回溯”。最经典的例子是^(a)$去匹配aaaaab。引擎在b之前会不停尝试各种a的分组方式aaa|aa、aa|aaa、a|aaaa……每一个组合都失败后才会放弃随着a的长度增加尝试次数呈指数级上涨。到50个字符就会卡到肉眼可见的延迟100个字符基本能把单线程CPU烧穿。我排查过一次线上事故一个“用户名是否合法”的正则接口正常响应1毫秒某天开始突然3秒超时查下来就是有人在用户名里输了一长串以a结尾的字符串正好触发了一个类似^(?:[a-z])$的模式。从那以后我给自己立了三条规矩能限定次数就限定次数比如{2,16}而不是能用字符类就别用.*叠加分组遇到用户可输入的长文本尽量在代码里先限制长度再走正则。另外一些语言支持“原子组”(?...)或占有量词*可以主动切断回溯路径遇到高性能要求时值得用但注意JavaScript和Python的re模块目前都不支持C#是支持的。4.3 常见问题排查实操指南我整理了五六个我平时被问得最多的正则“翻车现场”顺便给出定位思路正则在工具里匹配正常代码里却不匹配。先检查是不是没写^和$再看语言里是否需要对\做二次转义。JavaScript里你写new RegExp(\\d)需要有双反斜杠而字面量写法/\d/只需要一个。中文内容提取不到。检查\w是否包含中文最稳妥的做法是用[\u4e00-\u9fa5]这类显式unicode范围或者改用[^\s]这种非空白类。同一条正则在一个页面能匹配两次在另一个页面只匹配一次。八成是没开全局标志g或matches()与search()用混了。Python里re.match只匹配开头re.search才匹配任意位置这是经典混淆点。想提取“两个符号之间”的内容结果多了一大截。基本就是贪婪.*在作祟换.*?或精确字符类即可。邮箱、URL校验太严或太松。我见过有人把邮箱正则写成一两百个字符连xn--punycode 域名都要处理实际项目够用就好。校验的底线是“大概率拦截误输入不阻挡正常用户”与其死磕正则不如发个验证邮件。5. 推荐工具与调试方法别再用记事本写正则了正则这个东西靠肉眼盯是不现实的。我平时会用几类工具提效第一类是可视化调试器我用的比较多的是 regex101 和 regexr它们能实时展示匹配结果、捕获组、甚至逐步回溯的过程。尤其是回溯可视化对理解灾难性回溯特别有帮助你能清清楚楚看到引擎在哪个分支上浪费了多少步。第二类是各语言自带的调试技巧。Python 里我会用re.compile(r..., re.DEBUG)查看编译后的指令序列虽然对新手有点深但偶尔能帮你发现正则被解析成了什么结构。JavaScript 里我习惯直接用浏览器console 跑/\d/.exec(text)配合console.time粗测性能。C# 中Regex有个RegexOptions.Compiled在正则被反复使用时能显著提速但第一次构造会变慢适合长生命周期对象。另一个经验是正则写完之后一定要在“正常数据、边界数据、异常数据”三组样本上跑一遍。正常数据证明它能匹配边界数据比如空字符串、单字符、超长字符串、包含中文小括号的字符串这些最容易暴露贪婪和断言问题。异常数据则要故意往里面塞#、连字符、换行这些特殊符号用它们来验证你的边界定义够不够严谨。6. 我在实际项目里养成的三个正则习惯说到最后分享几个我坚持了很多年的习惯不算什么高深技巧但每一条都帮我省过真实的线上事故。第一正则能局部匹配就绝不全局扫描。很多人习惯拿到文本就.*一把梭其实先做字符串分割再用正则匹配单个片段往往更清晰也更高效。比如提取日志里的关键字段我会先用换行拆成行再逐行匹配而不是写一个横跨多行的巨型正则。第二永远优先考虑“可读性”。我在代码评审里见过太多一行二三百个字符的正则链接上自带一长串量词和断言除了作者本人没人能维护。现在我更倾向于把正则拆成小段用常量拼接甚至写注释说明每一段的作用。像 Python 的re.VERBOSE模式就是干这个的JavaScript 虽然没有原生支持但你可以用数组join或者模板字符串来拼再在注释里留好中文说明。这种可读性投入在几个月后回来看自己代码时回报率极高。第三正则解决不了的事情趁早换代码解决。这不是示弱是成熟。比如“判断一个字符串是不是合法IP”拿现成的库函数不香吗比如“HTML里取正文”先上解析器比正则写半年靠谱得多。正则最擅长的是格式规整的字符串一旦遇到嵌套结构HTML标签、括号表达式或者语义判断大于某个数值、日期是否真实存在它就该退居二线了。我一直跟团队说正则是工具箱里很好用的一把螺丝刀但它不是锤子更不是瑞士军刀。把常用正则这些套路练熟日常工作里至少能省掉一半文本处理时间。你可以把上面那二十来个表达式直接存成自己的代码片段遇到对应场景直接调等用熟了再开始自己组合和优化。如果你在实操中遇到某个“怎么都写不对”的正则别硬扛先拆需求、画边界再一步步补条件往往比你脑子里凭空推演要快得多。