正则表达式在线测试实用手册:5 个写法与 3 个坑

正则这东西不常写,写完三个月就忘。每次用到都是「翻旧项目 → 复制 → 改一半 → 在控制台里猜十分钟」。正则表达式在线测试存在的意义,就是把这个「猜」的过程压缩到几秒钟。

这篇按「最小语法集 → 五个能直接抄的写法 → JS 特有陷阱 → 速查清单」的顺序写,文中的每个表达式都在 Node 22 里跑过,输出结果直接贴在代码注释里。

一、正则表达式在线测试之前,先背下这张表

正则 90% 的需求只用到下面这些符号:

类别 写法 含义
字符类 \d \w \s 数字 / 单词字符 [A-Za-z0-9_] / 空白符,大写 \D \W \S 表示取反
. 任意字符(默认不含换行,加 s 标志才含)
[abc] [^abc] 集合中的任一个 / 非集合中的任一个
[a-z0-9] 范围,连字符放最前或最后才表示字面量 -
量词 * + ? 0 次以上 / 1 次以上 / 0 或 1 次
{n} {n,} {n,m} 恰好 n 次 / 至少 n 次 / n 到 m 次
锚点 ^ $ \b 开头 / 结尾 / 单词边界
分组 (...) 捕获组,可在结果里按 $1 取出
(?:...) 非捕获组,只分组不占编号
(?<name>...) 命名捕获组,结果里走 m.groups.name
| 或,优先级最低,常用 (?:a|b) 包起来
转义 \ \$ 匹配字面 $,在字符串里要写成 '\\$'

记住一条铁律:字符类内部大部分符号不需要转义。写 [/.:] 是合法的,只有 ] \ ^(首位时)-(表范围时)需要处理。

二、贪婪还是惰性,差一个 ?

量词默认贪婪:能匹配多长就匹配多长,然后才回头。加 ? 变成惰性:能匹配多短就匹配多短。

'<a>1</a><b>2</b>'.match(/<.+>/)     // '<a>1</a><b>2</b>'  ← 一路吃到最后的 >
'<a>1</a><b>2</b>'.match(/<.+?>/g)   // ['<a>', '</a>', '<b>', '</b>']

想写「匹配到第一个分隔符为止」,就用 <[^>]+> 这种排除型字符类,它比 <.+?> 更快也更不容易出错——惰性靠回溯实现,排除型字符类不需要回溯。

三、五个高频写法,直接抄

注意:下面每个表达式都写了 ^$,那是给「整串校验」用的。 如果你想从一段文本里把目标挑出来,把两端的锚点去掉,只留中间部分。

1. 手机号

^1[3-9]\d{9}$

第一位的 1 是号首,第二位限制在 3–9,把 1112 这类服务号段(110、120)挡在外面,剩下 9 位数字。一共 11 位。

实测:13812345678 通过,128123456781381234567138123456789+8613812345678 全部拒绝。带国家码要先 replace+86 或空格再校验。

2. 邮箱

^[\w.+-]+@[\w-]+\.[\w.]+$

邮箱的 RFC 规范复杂到某种程度上无法用正则完整表达(带引号的本地部分、注释、IP 字面量形式)。上面这个覆盖了绝大多数真实地址,包括多级域名。真正验证邮箱存在与否只能发一封确认邮件,不要再往这个表达式里加更多料了。

3. IPv4

^((?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$

四段拆分是或非关系:25[0-5](250-255)| 2[0-4]\d(200-249)| 1\d{2}(100-199)| [1-9]?\d(0-99,且不接受 01 这种前导零形式)。

这里的 (?:...) 很关键。如果写成捕获组 (...) 再配 {3} 重复,它只会记住最后一次匹配的那一组$1 拿不到你想要的四段。不需要取值的分组一律写成 (?:),既省内存也避免编号污染。

4. 日期(不做闰年)

^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$

它能卡住月份 01-12、日 01-31,但放行 2026-02-30。跨月边界必须交给 Date 对象或者日期库判断:

const ok = (s) => {
  if (!/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/.test(s)) return false
  const [y, m, d] = s.split('-').map(Number)
  return new Date(y, m - 1, d).getDate() === d   // 30 号会被 2 月纠正成 2 号,于是判假
}

5. 提取 URL 和 URL 参数

https?://[^\s]+

排除型字符类 [^\s].*? 安全:遇到中文、标点、空格自动停下。要取参数:

const str = 'https://it997.com/tool/regex-test?a=1&b=hello%20world&c='

// 首选:别自己造轮子
const params = Object.fromEntries(new URLSearchParams(str.split('?')[1] || ''))

// 必须用正则时(比如从一段混杂文本里捞)
for (const m of str.matchAll(/[?&]([^=&]+)=([^&]*)/g)) {
  console.log(m[1], '=', decodeURIComponent(m[2]))
}

👉 正则表达式在线测试(实时高亮匹配结果,列出每个捕获组)

把表达式和测试文本粘进去,右侧会显示命中位置 index$1$2 各捕获了什么,未命中的组标记为「未匹配」。工具内置了手机号、邮箱、IPv4、日期、中文等八组常用预设,可以点一下直接载入——但请留意:预设里都不带 ^$,它们是给「从文本中查找」用的,拿去做表单校验要自己两头加锚点。

四、JS 里躲不开的三个坑

坑 1:matchg 会丢掉捕获组

'abc123def456'.match(/(\d{3})/)       // 有组:['123', '123', index: 3, ...]
'abc123def456'.match(/(\d{3})/g)      // 无组:['123', '456'] —— 只剩整串匹配
'abc'.match(/\d/g)                    // null,别忘判空

gmatch 只返回完整匹配数组,捕获组、index 全部丢失。想同时拿到多次匹配和它们的组,用 matchAll必须带 g,否则抛 TypeError):

for (const m of 'abc123def456'.matchAll(/(\d{3})/g)) {
  console.log(m[0], m[1], m.index)
}
// 123 123 3
// 456 456 9

matchAll 返回迭代器,要数组就包一层 [...]

坑 2:testg 会忽真忽假

g 的正则对象是有状态的,它在自身上记录一个 lastIndex,每次匹配从那里继续:

const re = /\d{3}/g
re.test('abc123')   // true    lastIndex → 6
re.test('abc123')   // false   从 6 继续,找不到,lastIndex 归零
re.test('abc123')   // true

这就是生产环境里那种「同样的输入,结果隔一次变一次」的鬼故事。结论:校验用的正则不要加 g,需要全局搜索时用字面量或者每次新建对象,别把带 g 的正则对象缓存起来复用。

坑 3:中文匹配的范围

[\u4e00-\u9fa5] 覆盖的是 CJK 基本区(最常见的两万多汉字),够日常用,但覆盖不了扩展区的生僻字和兼容区的符号。现代浏览器可以用 Unicode 属性转义:

/[\u4e00-\u9fa5]+/.test('你好')            // true
/\p{Script=Han}+/u.test('你好世界 OK')     // true,需带 u 标志

注意 \w\d 默认只认 ASCII。要匹配中文用户名,得写成 [\u4e00-\u9fa5\w]+ 这种显式组合。

五、灾难性回溯:一个正则能把 CPU 打满

这是正则最严重的性能问题。看这个表达式:

const input = 'a'.repeat(28) + '!'
/(a+)+$/.test(input)   // 本机 Node 22 实测:约 31 秒才返回 false

(a+)+嵌套量词:外层的 + 可以重复内层的 a+,而中间的 a 有指数量级的拆分方式。碰到末尾那个 ! 导致失败时,引擎会穷举所有拆分组合再逐个回溯。

本机 Node 22 实测(a 个数 → 耗时):

输入长度 耗时
20 个 a 16 ms
24 个 a 260 ms
26 个 a 1.4 s
28 个 a 31 s

每多两个字符,耗时放大十倍量级。用户在表单里粘一段 30 个字符的文本就能把你的 Node 进程单线程卡死,这是一种标准的拒绝服务攻击(ReDoS)。

规避办法:

  1. 绝不写嵌套量词(a+)+(\d+)*(.*)+ 这类结构一律拆开,上面那个表达式的本意其实用 /^a+$/ 就够了
  2. 能用排除型字符类就别用 .[^>]+ 永远比 .*? 安全
  3. 给重复定上限:{1,64} 而不是 +
  4. 限制输入长度再喂给正则——这是最有效也最容易被漏掉的一条
  5. 不可信输入跑复杂正则时,放进有超时或有独立线程池的环境

六、手机号校验别写「万能正则」

^1[3-9]\d{9}$ 只能证明「这个字符串长得像手机号」,证明不了「这是个能收短信的号码」。矛盾在于:

  • 工信部每年都会放新的号段(16x、19x、17x 陆续开通),把表写进正则就意味着每年都要改一次代码
  • 写得太严(锁定到三号位)会把合法用户挡在门外;写得太松形同虚设
  • 前端挡住的永远是诚实的用户,挡不住批量注册

推荐做法是一套白名单前置 + 服务兜底

环节 做什么
前端 只做宽松前置:去掉空格与 - 后判断「11 位数字且以 1 开头」,最多再查一遍第二位
后端 同样只做格式检查,不维护完整号段表
真实校验 发一条短信验证码,收得到才算数

同理适用于身份证(18 位末位校验位要算校验和,正则算不了)、邮箱(要发确认函)、银行卡号(要走 Luhn 校验)。正则的职责是「快速拦截明显的错误」,不是「证明正确性」。

七、常用表达式速查表

需求 表达式(带 ^$ 为整串校验,查找时去掉锚点)
手机号 ^1[3-9]\d{9}$
邮箱 ^[\w.+-]+@[\w-]+\.[\w.]+$
IPv4 ^((?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$
日期 YYYY-MM-DD ^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$
时间 HH:mm:ss ^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$
身份证(18 位粗筛) ^\d{17}[\dXx]$
中文字符 [\u4e00-\u9fa5]
URL https?://[^\s]+
HTML 标签 <[^>]+>
正整数 / 非负整数 ^[1-9]\d*$ / ^\d+$
强密码(位置不限含四类) ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$

最后一行的 (?=...) 是零宽先行断言,不消耗字符,可以叠加多个条件。

八、写完正则的排错清单

  1. 先确认目的:整串校验加 ^ $,从文本里查找则不加
  2. 用带超时和样本的工具跑一遍,样本至少包含三类:应该匹配的、应该不匹配的、边界值(空串、超长、多一个字符)
  3. 检查有没有嵌套量词 (\w+)+,有就重写
  4. 校验用正则不带 gtest 复用的对象更不能带 g
  5. 要取多次匹配就用 matchAll,并且记得它可能返回空迭代器
  6. . 不匹配换行,需要跨行时加 s 标志或写成 [\s\S]
  7. JS 字符串里 \d 要写成 '\\d',否则 \d 会被当成转义吃掉——推荐直接用字面量 /\d+/
  8. 中文、emoji 相关的都要确认是否开了 u 标志
  9. 输入来自用户的,先限长再匹配
  10. 看不懂自己写的表达式时,拆成几个小正则分步处理,比一行天书更容易维护

相关工具:正则表达式在线测试JSON 格式化时间戳转换

全部纯前端实现,输入的内容不上传服务器。

还有 53 个免费在线工具

纯前端实现,不用注册,数据不上传服务器。

浏览全部工具