正则这东西,几乎是每个写代码的人迟早都要碰的“硬骨头”。你可能是因为表单校验、日志提取、爬虫解析,或者只是想把一段乱七八糟的文本里藏着的数字和符号抠出来,才点开了这篇内容。先说清楚,这篇文章不是教材,是我自己多年踩坑、翻文档、写工具积累下来的常用正则表达式清单和实操思路,覆盖JavaScript、Python、C#这几个主流语言里最常见的用法。不管你是刚接触正则的新手,还是已经写了几年代码但每次遇到复杂匹配都要重新翻手册的老手,这篇都值得收藏备用。
我在工作中最常见的需求无非这么几类:校验用户输入(邮箱、手机号、身份证这类格式)、从日志或网页中提取关键信息(数字、日期、URL、特定标记后的字符串)、以及清洗脏数据(去掉空白、去重、替换敏感词)。如果你也有类似的场景,往下看就对了。开场不废话,直接把最核心的匹配原理、二十来个高频正则、以及一个完整的“提取中间数字和#号后字符串”实战案例拆给你看。
1. 正则表达式的底层逻辑:先看破,再写不慌
1.1 正则到底是怎么匹配字符串的
很多初学者一上来就背语法,结果写出来的正则又长又错。我自己觉得,先弄明白正则引擎是怎么工作的,比背一百条规则都管用。简单说,正则表达式的核心就是“按模式逐个字符地扫描目标字符串”,引擎从字符串的某个位置开始,尝试用你的模式去匹配,匹配不上就回溯换一个位置,直到找到符合条件的子串或者明确失败。
这个过程中最关键的概念叫“回溯”。举个例子,你写\d+去匹配"abc123def",引擎会先让\d+尽量多吃数字(贪婪),吃到123之后发现后面是d不是数字,匹配结束,拿到123。但如果你的模式是\d+3,那么\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个正则整理成表。每个都可以在当前主流语言里直接用,但要注意校验类场景必须用^和$把两端锚定,否则你校验“邮箱”的时候,abc@qq.comxxx也会被当成合法值,因为引擎默认做的是“包含匹配”而不是“完全匹配”。
| 用途 | 正则表达式 | 匹配示例 |
|---|---|---|
| 邮箱 | ^[\w.+-]+@[\w-]+(\.[\w-]+)+$ | test.name+tag@abc.com.cn |
| 中国大陆手机号 | ^1[3-9]\d{9}$ | 13800138000 |
| 身份证号 | ^\d{17}[\dXx]$ | 110101199001011234 |
| IPv4地址 | ^((25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(25[0-5]|2[0-4]\d|1?\d?\d)$ | 192.168.1.100 |
| URL | ^https?://[\w-]+(\.[\w-]+)+[^\s]*$ | https://example.com/page?id=1 |
| 日期(年-月-日) | ^\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}$ | 200000 |
| QQ号 | ^[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+)$会直接匹配到末尾,也够用。
但假如你要提取"price=299.50, discount=19.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(r'PO-(\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>我的</title><p>博客</p><title>首页</title>,贪婪版本会从第一个<title>一直吞到最后一个</title>,中间全部当成标题。而懒惰版本.*?会在遇到第一个</title>时停下,正好取得“我的”。这算是我工作中遇到频率最高的一个坑。
4. 不同语言的平台差异:写错平台,代码全白搭
4.1 JavaScript、Python、C# 正则语法对比
同样一个正则,在不同语言里行为可能天差地别,这绝对不是危言耸听。我列个表把这几个最常踩的语言差异摆出来:
| 差异点 | JavaScript | Python | C# |
|---|---|---|---|
| 全局匹配 | 使用g标志,match才能拿到全部匹配 | 使用re.findall/re.finditer | Regex.Matches天然返回全部匹配 |
| 命名为捕获组 | (?<name>...)或(?'name'...)(新版支持) | (?P<name>...) | (?<name>...) |
| 分组引用 | 替换时用$1 | 替换时用\1或\g<1> | 替换时用$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标签、括号表达式)或者语义判断(大于某个数值、日期是否真实存在),它就该退居二线了。我一直跟团队说:正则是工具箱里很好用的一把螺丝刀,但它不是锤子,更不是瑞士军刀。
把常用正则这些套路练熟,日常工作里至少能省掉一半文本处理时间。你可以把上面那二十来个表达式直接存成自己的代码片段,遇到对应场景直接调,等用熟了再开始自己组合和优化。如果你在实操中遇到某个“怎么都写不对”的正则,别硬扛,先拆需求、画边界,再一步步补条件,往往比你脑子里凭空推演要快得多。