正则表达式测试与替换器

全球通用工具

正则表达式是编程里最常被人在语言之间复制粘贴的东西,而问题恰恰出在粘贴上:同样的字符在 Python 里是 SyntaxError,在 PCRE 里是另一种匹配结果,在 Go 的 RE2 里直接编译失败,因为这些风格不是同一种语言的方言,而是共享祖先的不同语言。所以这一页先在你的 JavaScript 真正会用的正则功能上运行模式,再把力气花在问题的另一半:这个模式在其他地方意味着什么。

用文本测试模式并读出每个匹配,或者看看替换串会产出什么——写正则通常就是为了这两件事。

裸模式,或者 `/pattern/flags` 字面量。它由这个浏览器里的引擎编译——也就是你自己的代码会用的那个——绝不由我们实现的东西编译。

g i m s u v y d。写在这里,或者写在上面字面量的闭合斜杠之后,此时这里的值会被忽略。

原样保留,包括换行——下面有几条解释正是关于换行、制表符和不换行空格的。

`(a+)+$` 这样的形状会花掉指数级时间,而浏览器无法中断已经开始匹配。页面拒绝这种形状并解释原因,而不是让你的标签页卡死。

留空则使用“regex-report.txt”,替换模式下是“replacement-result.txt”。

边输入边更新。所有计算都在你的浏览器里完成,数据不会离开本机。

匹配结果及其捕获组

2 个匹配

按 裸模式读取,使用标志“g”,在 93 个字符的文本上匹配。

标志
g
捕获组
user, host
第一个匹配位置
11

标出匹配位置的文本

Ana López <⟦ana.lopez@example.com⟧> wrote on 2026-03-14:
"Ship it" - ⟦bob@danfio.com⟧, 2026-03-15

每个匹配都用 ⟦ ⟧ 包起来——这些标记只为显示而加,不在你的文本里。

每个匹配及其捕获组

#位置匹配userhost
111ana.lopez@example.comana.lopezexample.com
267bob@danfio.combobdanfio.com

这些数字没有说明的事

  • `\w`、`\d` 和 `\b` 在 JavaScript 的任何模式下都是 ASCII,带不带 `u` 都一样:`\w+` 匹配 `café` 只得到 `caf`,`\bété\b` 和 `\bcafé\b` 什么都匹配不到,因为 é 不是单词字符,旁边也就没有单词边界。Python 3 正好相反——它的 `\w`、`\d` 和 `\b` 默认是 Unicode 感知的——模式在两者之间搬运时,含义就这样悄悄改变了。带 `u` 的 `\p{Letter}` 和 `\p{Number}` 才是这里 Unicode 感知的写法,而 `\s` 是唯一本来就 Unicode 感知的转义(包含 U+00A0 和 U+3000)。
  • g(全局):找出所有匹配,而不是在第一个匹配处停下。
  • 模式由这个标签页里的引擎编译——就是你的代码所运行的同一个 V8、JavaScriptCore 或 SpiderMonkey——匹配也完全由它完成,所以在这里表现如此的模式,在 `test`、`match`、`replace`、`split` 和 `search` 里表现相同。其他地方就是另一种语言:Python、Java、.NET、Go(RE2)、PCRE 和 grep 各自对哪些构造存在有不同答案,有时对 `$`、`\w` 或断言的含义也有不同答案。上面的发现会点名这个模式实际触及的差异。
  • 模式也是一个程序,写得不好的那个会是指数级的:`(a+)+$` 对着很长的一串 `a`,每多一个字符,耗时大约翻一倍,因为引擎会尝试把这一串切开的每一种方式。这个页面只拒绝它能确定识别出的那一种形状,不对别的发出警告,因为 `(a|aa)+` 一样昂贵,看起来却完全普通——静态分析看不到它。模式变慢时,几乎总是“被量化的分支”或“嵌套量词”,后者给了引擎一个它本不必做的选择。
  • 这里的位置是 UTF-16 偏移,与 `match.index` 给出的数字相同,可以直接搬进代码——有一个后果值得记住:基本平面之外的字符(emoji、罕见文字)算两个单元,而不带 `u` 的 `.` 只匹配其中一个的一半。
编译与匹配都在这个浏览器标签页里完成:模式和文本从未离开,页面背后没有能接收它们的接口,也没有记录日志。

粘贴模式,粘贴文本,逐个读出匹配及其捕获组和位置。页面会点名那些能悄悄编译、含义却不同的构造(没有 `u` 时 `\p{L}` 匹配的是文本“p{L}”),在没有匹配时解释原因,并拒绝唯一一种能让标签页卡死的形状——因为 JavaScript 无法中断已经开始匹配。

关于这个工具

用你自己的文本测试模式,读出每个匹配的捕获组、位置和标志,再看看同一个模式在别处会怎样。这一页在 JavaScript 真正会用的正则功能上运行表达式,其余篇幅用来点名 Python、PCRE 或 RE2 里的差异。

所有匹配都在标签页里完成:模式和文本交给浏览器自带的正则功能,从不离开页面,当样本是真实日志或客户数据时,这一点很重要。页面不做任何改写,所以这里的结果与你生产环境里读到的一致。

有一种形状会被拒绝执行而不是硬跑:带嵌套量词、可以无限回溯的模式,因为 JavaScript 无法中断已经开始匹配,标签页会卡死。页面测试的是匹配与替换,它并不保护服务器免受同一个模式的影响。

它是怎么工作的

你测的是浏览器自带的正则,页面会说明是哪一个

正则表达式不是一种语言。JavaScript、Python、Java、.NET、PCRE、Perl、PHP、Ruby、grep、Go 的 RE2 和 Rust 的 regex crate 各自实现自己的模式语法,差异不是装饰性的:有些构造只存在于其中一种,比如原子组、占有量词、`\K`、`[[:alpha:]]`;有些构造在两种语言里含义不同,比如 `$` 是否匹配结尾换行之前、`\w` 是否包含带重音的字母、先行断言能看到什么。

你的模式直接在你自己的标签页里运行,用的是浏览器自带的 JavaScript 正则功能,所以在这里能工作的模式,在你代码里的 `test`、`match`、`replace`、`split` 和 `search` 中表现一致。页面不做模拟,也不做翻译;报告还会说明当前浏览器支持哪些特性,提 issue 时可以说清是哪个浏览器产生的。

浏览器帮不上忙的是另一个方向。面板会列出另一种风格才有的构造,并说明属于哪一种,这样从 Python 移植过来、或者照 PCRE 文档写出的模式就不再是谜。这部分通常是测试器会漏掉的,也正是会耗掉一个下午的部分。

标志位,以及最容易让人意外的三个

闭合斜杠后面的字母不是装饰,它们改变模式的含义。`g` 决定你得到一个匹配还是全部匹配,没有它 `replace` 只改第一处——这是 JavaScript 字符串里最常见的意外。`i` 做大小写折叠,用的是 Unicode 的 simple folding,所以 `ß` 仍然不匹配 `ss`,写成 `[a-z]` 的字符类也仍然只有 26 个 ASCII 字母。`m` 让 `^` 和 `$` 逐行匹配,而不是只匹配整个字符串的两端;`s` 让 `.` 能匹配换行——没有它,`.` 会在换行处停下,这就是为什么在一行内有效的模式,放到整个文件里一个都匹配不到。

最容易让人意外的三件事,其实与字符有关,而不是标志。第一:`\w`、`\d` 和 `\b` 在任何模式下都是 ASCII,所以 `\w+` 匹配 `café` 只得到 `caf`,而 `\bété\b` 什么都匹配不到,因为 é 不是单词字符,`\b` 却要求两侧都是单词字符。带 `u` 的 `\p{Letter}` 才是 Unicode 版本,Python 3 默认就是另一套,混淆正是从这里来的。

第二:不带 `m` 的 `$` 只匹配字符串的最末尾,所以 `/abc$/` 不匹配“abc\n”——Python 和 Perl 都会在那里匹配。第三:`y`(sticky)不是“从位置 0 开始然后继续搜索”,它只在 `lastIndex` 处匹配,别处都不试,对一个新的模式来说就只有一个位置。当循环里的 `^` 行为古怪时,人们想要的通常就是这个标志。

捕获组、位置,以及替换语法

捕获组记录它匹配到的内容,也可以带上名字:`(?<user>[\w.]+)` 既是第 1 组,也叫 `user`。`d` 标志会增加 `match.indices`,也就是整个匹配和每个组的起止位置——这是知道某个组*在哪里*匹配、而不只是知道它匹配了什么的方法;当正确的文本出现在错误的位置时,就该打开它。这里的位置是 UTF-16 偏移,与 `match.index` 给出的数字一致,所以一个 emoji 算两个单位,每个数字都可以直接搬进代码。

替换字符串有自己的一门小语言,两种习惯在这里碰撞。在 JavaScript 里 `$1` 是第 1 组,`$&` 是整个匹配,`$<name>` 是命名组,`$$` 是字面的美元符号,`` $` `` 是匹配之前的所有内容,`$'` 是匹配之后的所有内容。在 sed、grep 和 Perl 里,反向引用写作 `\1`,整个匹配是 `&`;在 .NET 里整个匹配是 `$0`。JavaScript 对这些写法一律原样保留,所以从别的语言复制来的替换串会产出字面文本,而且完全不报错。

有一个区别比其余都值得记住,因为它看起来像什么都没有:引用一个不存在的组,会被当成文本复制到输出里,`$2` 还是 `$2`;而引用一个存在、但这次匹配没有参与的组,会变成空字符串。两个只差一个字符的替换串,行为可能完全不同,而且都不会抛异常。

为什么一个模式能卡死标签页,这一页怎么处理

正则表达式是一个程序,写得不好的那个会慢得让习惯了循环的人吃惊。`(a+)+$` 遇上很长的一串 `a`,再跟一个不匹配的字符,引擎会尝试把这一串切成两份、三份、四份的每一种方式:每多一个字符,工作量就翻一倍,所以三十个字符的输入大约是十亿次尝试。什么都没有坏,也没有报错——标签页只是不再响应。

能避开这个问题的引擎,都是靠放弃特性换来的。RE2、Go 的 `regexp` 和 Rust 的 regex crate 通过拒绝反向引用和先行、后行断言,做到匹配时间与输入长度成正比,这也是同一个模式在 Go 里是线性的、在 JavaScript 里却是灾难的原因。在回溯引擎里,解决办法是让每一次重复都消耗掉一个无法退回的字符——把一段连续内容匹配一次,再检查它后面跟着什么,而不是把一个量词嵌进另一个量词里。

JavaScript 无法中断正在进行的匹配:没有超时可以设置,也没有办法中途终止 `RegExp`,所以“跑一下看看”回答的是错误的问题。因此页面会拒绝它能确定识别出的那一种形状——量词作用在已经含有无界量词的组上——把光标指过去、解释清楚,并为输入确实只有几十个字符的情况留一个开关。它也明说静态检查看不到什么:`(a|aa)+` 一样昂贵,看起来却完全无害。

示例

示例使用页面自带的默认输入,只改设置;下面的结果与你在页面上看到的完全一致。

两个地址,两个命名组

要做什么: 测试模式:匹配、捕获组和位置 · 正则表达式: (?<user>[\w.%+-]+)@(?<host>[\w.-]+\.[A-Za-z]{2,}) · 标志: g · 要搜索的文本: Ana López <ana.lopez@example.com> wrote on 2026-03-14: "Ship it" - bob@danfio.com, 2026-03-15

匹配结果及其捕获组

2 个匹配

按 裸模式读取,使用标志“g”,在 93 个字符的文本上匹配。

标志: g · 捕获组: user, host · 第一个匹配位置: 11

两个匹配都会把 `user` 和 `host` 分开返回,这正是命名组的用途:结果里的列表头写的是 `user` 和 `host`,而不是 `1` 和 `2`,替换时也可以写 `$<user>` 而不是 `$1`。

悄悄编译通过的方言陷阱

要做什么: 测试模式:匹配、捕获组和位置 · 正则表达式: \p{L}+ · 标志: g · 要搜索的文本: café 日本語

没有匹配

没有匹配

按 裸模式读取,使用标志“g”,在 8 个字符的文本上匹配。

标志: g · 捕获组: 无 · 第一个匹配位置: 无

没有 `u` 标志时,`\p` 被读成普通字母 `p`,于是它去找文本“p{L}”,什么也找不到——没有报错,没有匹配,也没有线索。加上 `u`,同一个模式就能匹配所有字母,包括重音字母和中文。

用命名组查找并替换

要做什么: 查找替换:$1、$<name>、$& 等等 · 正则表达式: (?<user>[\w.%+-]+)@(?<host>[\w.-]+\.[A-Za-z]{2,}) · 标志: g · 要搜索的文本: ana.lopez@example.com and bob@danfio.com · 把每个匹配替换为: $<user> at $<host>

替换后的结果

2 次替换

每个匹配都被替换了,因为模式带有 `g`。

标志: g · 找到的匹配: 2 · 替换次数: 2

加上 `g`,两个地址都会被改写。删掉 `g`,只有第一个会变,这就是 `replace` 与 `replaceAll` 的差别,一次按键就能看见。

常见问题

这里的正则和 Python、Java 或 PCRE 一样吗?

不一样,而这正是这个页面的重点。它用的是 JavaScript 自己的引擎(ECMA-262),也就是浏览器和 Node 运行的那一个。Python 的 `re`、Java 的 `Pattern`、.NET、PCRE、Perl、Ruby 和 PHP 各自实现自己的语法,Go 的 RE2 和 Rust 的 regex crate 实现的是一套刻意更小的语法。有些构造只在其中一种里存在——原子组 `(?>...)`、占有量词 `a++`、`\K`、`[[:alpha:]]`、自由间距的 `x` 标志——有些构造含义不同,比如结尾换行前的 `$`、带重音字母的 `\w`、替换串里的 `$0`。面板会点名你的模式触及的差异,这比一张兼容性表有用。

为什么我的模式在这里能匹配,在 Python 里却不行?

通常是四件事之一。`\w`、`\d` 和 `\b` 在 JavaScript 里是 ASCII,在 Python 3 里是 Unicode 感知的,所以为中文或带重音文本写的模式在两个语言里表现不同。`$` 在 Python 里匹配结尾换行之前,在 JavaScript 里只匹配最末尾。命名组在 Python 里写作 `(?P<name>...)`,这里是 `(?<name>...)`。还有 Python 的 `re.match` 从开头锚定,JavaScript 的 `test` 在任意位置搜索,所以同一个模式在一种语言里找到匹配,在另一种里找不到。每一条都在页面上写明,而不是留给你自己发现。

什么是灾难性回溯,怎么避免?

当模式给引擎一个可以用指数级方式展开的选择时,就会发生。`(a+)+$` 遇上很长的一串 `a` 是经典例子:内层的 `+` 和外层的 `+` 可以把这一串切成两段、三段或四段,每一种切法都会在放弃之前试过。`(a|aa)+` 一样糟糕,看起来却人畜无害。解法是让每一次重复都消耗掉无法退回的内容:把连续段匹配一次再检查后续,能用字符类的地方就别用通配符,并记住*失败*的输入才昂贵——模式在匹配成功的文本上很快。RE2、Go 和 Rust 的正则引擎通过放弃反向引用和先行、后行断言彻底避开这个问题;浏览器做不到,所以只能识别形状。

为什么点号匹配不到我的换行?

默认情况下 `.` 匹配除行终止符以外的任意字符,JavaScript、Python、Java、.NET 和 PCRE 都是如此——所以在一行里写的 `.*` 放到文件里一个都找不到,人们的第一反应往往是怪文本。`s` 标志(dotAll)会改变这一点,`[\s\S]` 则是不需要标志、在所有 flavor 里都能用的写法。当模式用了 `.` 而文本不止一行时,页面会把这条解释放在“没有匹配”旁边,因为这几乎总是答案。

为什么 \w 不匹配重音字母或中文?

因为 `\w` 在 JavaScript 里就是 `[A-Za-z0-9_]`,任何标志组合都一样——加上 `u` 也不会改变它,而这恰恰是很多人以为能修好的地方。对 `\w+` 来说,`café` 只是 `caf`;`\bcafé\b` 什么都匹配不到,因为引擎不把 é 当作单词字符,它旁边也就没有单词边界。Unicode 感知的写法是带 `u` 标志的 `\p{Letter}`,数字用 `\p{Number}`。Python 3 默认就是 Unicode 感知的,所以同一个模式在两种语言之间搬运时,含义会悄悄改变。

为什么只替换了第一个匹配?

因为模式没有 `g` 标志。用正则调用 `String.prototype.replace` 只替换第一处,而同一个方法用*字符串*作为查找值时替换全部——同一个方法的两半行为不一致,混淆就是从这里来的。解决办法是加 `g` 标志,或者用 `replaceAll`,后者对非全局模式会直接拒绝,而不是做出意外行为。这里的替换模式严格按照你写的标志执行,把差异展示出来,而不是替你抹平。

d 标志增加了什么,替换里的 $& 又是什么?

`d`(indices)会加上 `match.indices`:整个匹配和每个组的起止位置,这是知道一个组在哪里匹配的唯一方法——报告会把它打印成一张范围表。在替换串里,`$&` 是整个匹配,`$1` 和 `$<name>` 是捕获组,`$$` 是字面的美元符号,`` $` `` 是匹配之前的所有内容,`$'` 是匹配之后的所有内容。人们常写的两种东西根本不是引用:`$0`,.NET 用它表示整个匹配,JavaScript 把它当文本;以及 `\1`,sed、grep 和 Perl 用它,JavaScript 把它当两个字符。

在这里粘贴模式和自己的文本安全吗?

安全,这也正是在浏览器里运行、而不是放到服务器上的理由。所有内容都在这个标签页里由 JavaScript 编译和匹配:页面背后没有能接收模式、文本或替换串的接口,没有日志,输入时也不发请求。你最初加载的静态 HTML 里就已经包含真实结果,如果匹配发生在别处,这不可能做到。文本最多搜索 100,000 个字符,最多列出 500 个匹配,触及任一上限时面板会说明。

资料来源与统计口径

  • 正则由浏览器自带的 JavaScript 正则功能执行,语法与八个标志都遵循 ECMA-262,和你在自己代码里用的是同一套规则。
  • 页面不会执行任何代码;可能指数级变慢的模式会在运行前被拦下,你也可以明确选择继续运行。
  • 模式与文本只留在浏览器里,不会上传。

最近复核于 2026-09-16.

本页在您的浏览器本地运行,请保留原文件备份;不涉及财务建议。 阅读完整免责声明。