正则不是用来"匹配一切"的,是用来把不可能的输入挡在更前面。下面这些写法都遵循一个原则:先用 ^ 和 $ 锚定整个串,再谈内容。少了锚定,\d{11} 会在 20 位的字符串里也匹配成功。
| 用途 | 正则 | 说明 |
|---|---|---|
| 中国大陆手机号 | ^1[3-9]\d{9}$ |
11 位,第二位 3–9 |
| 邮箱(宽松实用) | ^[\w.+-]+@[\w-]+(\.[\w-]+)+$ |
不追求 RFC 5322 全兼容 |
| 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 |
| 日期 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$ |
|
| 整数或小数 | ^-?\d+(\.\d+)?$ |
|
| 中文字符 | [\u4e00-\u9fa5] |
常用汉字区;更完整用 \p{Script=Han} 加 u 标志 |
| 首尾空白 | ^\s+|\s+$ |
用于 trim |
| 连续空行 | \n{3,} |
替换成 \n\n |
| HTTP(S) URL | ^https?://[^\s]+$ |
宽松判断,别指望它验证域名合法性 |
IPv4 那段看起来长,但拆开就是"0–255 的四段,用点连接"。写成 \d{1,3} 会把 999.999.999.999 也放过去。
* 和 + 默认贪婪,会吃掉尽可能多的字符。从 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。
下面这个正则看着无害,但对特定输入会指数级回溯:
/^(a+)+$/.test('aaaaaaaaaaaaaaaaaaaaaaaaaaab'); // CPU 直接跑满
原因是嵌套量词 (a+)+ 在匹配失败时要枚举所有切分方式,长度每加 1,回溯次数翻倍。真实项目里更常见的触发形式是 ([\w]+)*、(\s*\w+)* 这类组合。
应对办法,按优先级:
(\d+,)*\d+ 改成 \d+(,\d+)*。str.length <= 64。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_],能省掉一整类"本地测得好好的,上线就开始漏"的问题。
从长到短拆。一个复杂正则匹配不上时,先砍掉后半段看前半段能不能匹配,逐段加回来,定位到第一个失配的位置。这比盯着整条表达式猜快得多。
另外三个值得养成的习惯: