开发者最常用的正则表达式写法

正则不是用来"匹配一切"的,是用来把不可能的输入挡在更前面。下面这些写法都遵循一个原则:先用 ^ 和 $ 锚定整个串,再谈内容。少了锚定,\d{11} 会在 20 位的字符串里也匹配成功。

常用模式速查

下面这张表按用途分了七组,每条都给了完整表达式,带 ^ 和 $ 的是校验用的(要求整串匹配),不带的是从一段文本里抽取用的,两者不要混着用——把抽取用的 \d{4}-\d{2}-\d{2} 拿去做表单校验,2024-01-15abc 也会被判为通过。

联系方式

用途 正则 说明
中国大陆手机号 ^1[3-9]\d{9}$ 11 位,第二位 3–9
邮箱(整串校验) ^[\w.+-]+@[\w-]+(\.[\w-]+)+$ 不追求 RFC 5322 全兼容
邮箱(从文本抽取) \b[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}\b 不加锚定,用于从一段话里捞
座机(区号可选) ^(\d{3,4}-)?\d{7,8}$ 如 010-12345678
邮政编码 ^[1-9]\d{5}$ 6 位,首位非 0

数字与金额

用途 正则 说明
非负整数 ^\d+$ 0 和正整数
整数或小数 ^-?\d+(\.\d+)?$ 允许负号
最多两位小数 ^-?\d+(\.\d{1,2})?$ 金额场景最常用
带千分位的金额 ^[1-9]\d{0,2}(,\d{3})*(\.\d{1,2})?$ 如 10,000.00
恰好 n 位数字 ^\d{n}$ 把 n 换成具体数字
十六进制颜色 ^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ 支持 #fff 和 #aabbcc

日期与时间

用途 正则 说明
日期 YYYY-MM-DD ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$ 只校验形状,不校验闰年
时间 HH:mm ^([01]\d|2[0-3]):[0-5]\d$ 24 小时制
时间 HH:mm:ss ^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$ 带秒
年月 YYYY-MM ^\d{4}-(0[1-9]|1[0-2])$
日期(从文本抽取) \d{4}-\d{2}-\d{2} 不加锚定

中文与字符

用途 正则 说明
单个中文字符 [\u4e00-\u9fa5] 常用汉字区
整串中文 ^[\u4e00-\u9fa5]+$ 更完整用 \p{Script=Han} 加 u 标志
中文英文数字下划线 ^[\u4e00-\u9fa5A-Za-z0-9_]+$ 用户名场景常用
首尾空白 ^\s+|\s+$ 用于 trim
空白行 ^\s*$ 配合 m 标志逐行处理
连续空行 \n{3,} 替换成 \n\n

证件与编码

用途 正则 说明
身份证号 (^\d{15}$)|(^\d{18}$)|(^\d{17}[\dXx]$) 15 或 18 位,末位可为 X
银行卡号 ^\d{12,19}$ 只校验位数,不校验 Luhn
统一社会信用代码 ^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$ 18 位
普通车牌 ^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼][A-Z][A-HJ-NP-Z0-9]{5}$ 只覆盖蓝牌小型车

网络与文件

用途 正则 说明
IPv4 ^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$ 逐段限定 0–255
HTTP(S) URL ^https?://[^\s]+$ 宽松判断,别指望它验证域名合法性
域名 ^[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$ 支持多级
图片文件名 (\w+)\.(jpg|png|gif|webp) 用于从文本抽取

账号与密码

用途 正则 说明
用户名 ^[a-zA-Z][a-zA-Z0-9_]{3,15}$ 字母开头,4–16 位
密码(字母开头) ^[a-zA-Z]\w{5,17}$ 6–18 位
强密码 ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$ 大小写 + 数字,至少 8 位

IPv4 那段看起来长,但拆开就是"0–255 的四段,用点连接"。写成 \d{1,3} 会把 999.999.999.999 也放过去。

强密码那条用了三个先行断言,读法是"从第 0 位开始,后面某处要有小写、某处要有大写、某处要有数字,整体至少 8 位"。断言不消耗字符,所以三条可以叠加。

贪婪和惰性

* 和 + 默认贪婪,会吃掉尽可能多的字符。从 HTML 里取标签内容时,贪婪版本会跨到最后一个标签:

'<a>甲</a><a>乙</a>'.match(/<a>(.*)<\/a>/)[1];   // '甲</a><a>乙'  贪婪
'<a>甲</a><a>乙</a>'.match(/<a>(.*?)<\/a>/)[1];  // '甲'           惰性

加一个 ? 就变成惰性。代价是回溯次数变多,长字符串上会慢。更稳的写法是"排除字符集":<a>([^<]*)</a> 根本不回溯。

三种括号的区别

写法 作用 是否进捕获结果
(...) 捕获组 是
(?:...) 非捕获组,只做分组 否
(?<name>...) 命名捕获组 是,groups.name
(?=...) / (?!...) 正向 / 负向先行断言 否
(?<=...) / (?<!...) 正向 / 负向后行断言 否

只是想让量词作用在一组字符上时,一律用 (?:...)。用 (...) 会白白污染捕获索引,尤其是嵌套多了以后 $3 到底是第几组很容易数错。

命名组在需要对多个字段取值时特别好用:

const m = '订单 2023-11-14 金额 ¥128.50'.match(/(?<date>\d{4}-\d{2}-\d{2}).*?¥(?<amount>\d+(\.\d{2})?)/);
m.groups.date;     // '2023-11-14'
m.groups.amount;   // '128.50'

断言用来表达"前后必须是某个东西,但不要把它取出来"。比如只匹配金额数字、不匹配人民币符号:

/(?<=¥)\d+(\.\d{2})?/.exec('合计 ¥128.50')[0];  // '128.50'

后行断言是 ES2018 才进 ECMAScript 的,跑在老浏览器或老 Node 上会直接抛语法错误。

两个容易搞混的标志

标志 影响
m ^ 和 $ 匹配每一行的行首行尾,而不只是整个串
s . 可以匹配换行符(默认不能)
i 忽略大小写
g 查找所有匹配;配合 String.replaceAll 或循环 exec
u 启用 Unicode 模式,\p{...} 才有意义

处理多行日志时,m 常常是必须的;处理跨行块(比如整段 SQL)时,需要 s。

ReDoS:正则也会把服务打挂

下面这个正则看着无害,但对特定输入会指数级回溯:

/^(a+)+$/.test('aaaaaaaaaaaaaaaaaaaaaaaaaaab'); // CPU 直接跑满

原因是嵌套量词 (a+)+ 在匹配失败时要枚举所有切分方式,长度每加 1,回溯次数翻倍。真实项目里更常见的触发形式是 ([\w]+)*、(\s*\w+)* 这类组合。

应对办法,按优先级:

  1. 消除嵌套量词。(\d+,)*\d+ 改成 \d+(,\d+)*。
  2. 限制输入长度。校验前先判断 str.length <= 64。
  3. 别让用户输入进复杂正则。尤其是把表单值拼进 new RegExp() 的场景。

JavaScript 不支持原子组和占有量词(PCRE、Java、.NET 支持),所以第 1 条在 JS 里是唯一根治手段。

两个该记住的原则

不要用一个正则解析嵌套结构。 HTML、JSON、带括号的数学表达式都不是正则语言,靠加 .*? 和断言硬凑出来的表达式一定会漏边界情况。用对应的解析器。

在字符串里写正则要小心双重转义。 new RegExp('\\d+') 才等价于字面量 /\d+/;写 new RegExp('\d+') 得到的是匹配字母 d 的正则。用 String.raw 可以避免这类错误:

const re = new RegExp(String.raw`\d{4}-\d{2}-\d{2}`);

跨语言的三个差异

同一个正则在不同引擎里可能行为不同:

差异点 说明
\d 的范围 JavaScript 的 \d 恒等于 [0-9];Python 3 的 \d 默认匹配 Unicode 十进制数字(含阿拉伯-印度数字等),加 re.ASCII 才退化为 [0-9]
\w 的范围 同理,JS 是 [A-Za-z0-9_],Python 3 默认含 Unicode 字母
后行断言 JavaScript 从 ES2018 起支持;Python 的 re 模块要求后行断言是定长,(?<=ab) 可以,(?<=a+) 会报错
标志写法 JS 用 /re/gi;Python 用 re.I、re.M;Go 用内联的 (?i)

校验类正则如果要跨语言复用,把 \d 显式写成 [0-9]、把 \w 显式写成 [A-Za-z0-9_],能省掉一整类"本地测得好好的,上线就开始漏"的问题。

调试正则的方法

从长到短拆。一个复杂正则匹配不上时,先砍掉后半段看前半段能不能匹配,逐段加回来,定位到第一个失配的位置。这比盯着整条表达式猜快得多。

另外三个值得养成的习惯:

  1. 先写用例再写正则。把"应该匹配的"和"不应该匹配的"各列五条,尤其要放边界输入:空串、超长串、带换行的串、前后带空格的串。
  2. 用带高亮和分组展示的测试工具。肉眼看每个捕获组取到了什么,比打日志快。
  3. 生产环境的正则加长度上限。前面说的 ReDoS,除了改写法,最省事的兜底就是在校验前先判断输入长度。

自己动手试试

上面任何一条正则都可以直接粘进在线正则表达式测试器:左边填正则和待测文本,右边实时高亮命中位置、列出每个捕获组取到的值,还能一键生成 JavaScript、Python、Java、Go 等六种语言的调用代码。不确定某条为什么匹配不上时,文本对比工具可以把你改前改后的两版正则并排放一起看。

调试的过程中,工具页自带的「语法速查」标签页把字符类、量词、锚点、分组、修饰符按类分好了,点一下就能插到光标处,不用再回头翻这张表。

相关工具