正则工作台 · 免费

正则表达式测试与调试工具

写下模式,粘贴你真正要处理的文本,输入时每个匹配项都会实时亮起,同时显示捕获组、命名组、偏移量和替换预览,并用通俗易懂的语言解释语法错误,而不是照搬运行时自己的措辞。

使用浏览器自带的 RegExp · 不上传任何内容

正则表达式工作区

3 个匹配,3 个捕获组。第一个匹配位于索引 9。

模式

写的时候不要加斜杠

也可以粘贴完整的字面量,例如 /\d+/gi,标志会一并带入。下方所有内容都会在每次按键时重新计算。

g

编译成功。3 个捕获组(year、month、day)· 模式长度为 70 个字符。

标志
测试文本中的匹配
3

/\b(?<year>\d{4})-(?<month>0[1-9]|1[0-2])-(?<day>0[1-9]|[12]\d|3[01])\b/g 已在 114 个字符上运行。

匹配
3
捕获组
3
命名组
3
零宽匹配
0
文本覆盖率
26.3%
运行时间
–
测试文本

模式要处理的文本

匹配项会在输入时着色。相邻匹配交替使用深浅底色,每个匹配的左边缘还有一条竖线,所以两个紧挨着的匹配不会被看成一个。

114 / 100,000 个字符 · 4 行

结果

每个匹配及其偏移量和捕获组。

匹配 #1 · 捕获组
索引 9 到 19
  • $1year2026
  • $2month07
  • $3day27

打开 d 标志,即可看到每个分组的位置,而不只是它的内容。

预设库

如实说明匹配范围的模式

每个预设都会载入它的模式、标志、替换内容,以及一段示例文本,其中包含它刻意拒绝的情况。

格式检查

带锚点的模式,检查整行是否符合格式。

在文本中查找

不带锚点的模式,从连续的正文或日志中提取条目。

改写

重点在于替换而不是搜索的模式。

速查表

每个语法符号、它的含义,以及能说明它的最小示例

专门针对 JavaScript 语法:注释会指出它与 PCRE 和 Python 不同的地方。

字符类

文本中的一个位置,以及允许填入该位置的字符集合。

.
除换行符外的任意字符,除非开启了 s 标志。/a.c/ 匹配“abc”和“a-c”,不匹配“a\nc”
[abc]
列出的字符中的任意一个。/[aeiou]/ 在“rhythm”中什么也找不到
[^abc]
任意一个未列出的字符。脱字符必须放在最前面。/[^0-9]/ 在“a1”中匹配“a”
[a-z]
一个范围,按码点顺序。/[a-f0-9]/ 表示一个十六进制数字
\d \D
一个 ASCII 数字,以及任何非数字字符。\d 恰好等于 [0-9];它不包括阿拉伯-印度数字或全角数字。/\d{4}/ 在“year 2026”中匹配“2026”
\w \W
单词字符 [A-Za-z0-9_] 及其补集。带重音的字母不是单词字符。/\w+/ 在“café”中匹配“caf”
\s \S
空白字符(空格、制表符、换行符、换页符、NBSP 以及 Unicode 空格分隔符)及其补集。/\s+/ 合并连续的空白
\p{…}
一个 Unicode 属性。需要 u 标志;没有它,\p 表示字面字母“p”。/\p{Lu}/u 匹配任意文字系统中的一个大写字母

量词

前面的内容可以重复多少次。默认是贪婪的;加上 ? 即可变为惰性。

*
零次或多次。总是能成功,所以 /a*/g 到处都能匹配。/ab*c/ 匹配“ac”和“abbbc”
+
一次或多次。/\d+/ 在“abc123”中匹配“123”
?
零次或一次:该原子是可选的。/colou?r/ 两种拼写都能匹配
{3}
恰好三次。/\d{3}/ 匹配“555”
{2,}
两次或更多,没有上限。/o{2,}/ 在“ooooh”中匹配“oooo”
{2,4}
两到四次。/a{2,4}/ 在“aaaaa”中匹配“aaaa”
*? +? ?? {n,m}?
惰性形式:尽可能少地匹配,只有在模式其余部分失败时才扩展。/<.+?>/ 在“<a><b>”中匹配“<a>”,而不是“<a><b>”

锚点与边界

零宽断言。它们不消耗任何字符,只判断位置。

^
文本开头;开启 m 标志时为任意一行的开头。/^ERROR/m 找出所有以 ERROR 开头的行
$
文本末尾;开启 m 标志时为任意一行的末尾。/;$/m 找出以分号结尾的行
\b
单词边界:位于 \w 和非 \w 之间。它基于 ASCII 单词字符定义,所以在“naïve”中会落在单词中间。/\bcat\b/ 不匹配“category”
\B
非单词边界。/\Bcat/ 匹配“bobcat”中的“cat”

分组、分支与引用

捕获能把一次测试变成提取出的数据,但在你不需要时也会白白占用内存。

(…)
捕获组,按左括号从左到右编号。/(\d+)-(\d+)/ 产生两个分组
(?:…)
非捕获分组。分组方式相同,但不在结果中占位。/(?:ab)+/ 重复“ab”,不捕获任何内容
(?<name>…)
命名捕获组,ES2018 引入。可通过 match.groups.name 访问。/(?<year>\d{4})/,然后用 match.groups.year
|
分支,从左到右尝试。第一个能让整个模式成功的分支胜出。/cat|category/ 在“category”中匹配“cat”
\1
反向引用:第 1 组已经匹配到的确切文本。/(\w+) \1/ 找出重复的单词
\k<name>
对命名组的反向引用。/(?<q>['"]).*?\k<q>/ 匹配成对的引号

环视

检查当前位置周围的文本但不消耗它的断言。

(?=…)
先行断言:后面的内容必须匹配。/\d+(?= kg)/ 在“80 kg”中匹配“80”
(?!…)
否定先行断言:后面的内容必须不匹配。/\bfoo(?!bar)/ 跳过“foobar”
(?<=…)
后行断言:前面的内容必须匹配。ES2018 引入,与大多数引擎不同,JavaScript 允许它是变长的。/(?<=£)\d+/ 在“£40”中匹配“40”
(?<!…)
否定后行断言:前面的内容必须不匹配。/(?<!un)happy/ 跳过“unhappy”

标志

作用于整个正则。JavaScript 没有内联的 (?i) 修饰符:标志要么处处生效,要么完全不生效。

g
全局:查找所有匹配,lastIndex 在多次 exec 调用之间保留。/a/g 配合 matchAll
i
不区分大小写。/hello/i 匹配“HELLO”
m
多行:^ 和 $ 在换行处匹配。不影响点号。/^\s*$/m 找出空行
s
dot-all:. 也匹配换行符。/<p>.*<\/p>/s 可以跨行
u
Unicode 模式:码点、\p{…}、\u{…} 以及严格转义。/^.$/u 匹配单个 emoji
y
粘性:只在 lastIndex 处匹配,不向前扫描。词法分析器手动推进 lastIndex
d
索引:match.indices 为每个分组保存 [start, end]。match.indices[1] 定位第 1 组

转义与字面量

有十二个字符是特殊字符。要按字面匹配其中一个,请在它前面加反斜杠。

\. \* \+ \?
按字面理解的元字符。完整集合是 . * + ? ^ $ { } ( ) | [ ] \/\d+\.\d+/ 匹配“3.14”
\\
字面反斜杠。在字符串字面量中还要再加倍,写成 \\\\。new RegExp("\\\\d") 等同于 /\\d/
\n \r \t
换行符、回车符、制表符。/\r?\n/ 可按任一种行尾拆分
\xFF
用两位十六进制代码表示一个字符。/\x41/ 匹配“A”
\u00e9
用四位十六进制代码表示一个字符。/\u00e9/ 匹配“é”
\u{1F600}
U+FFFF 以上的码点。需要 u 标志。/\u{1F600}/u 匹配一个 emoji
\0
NUL 字符。它不是八进制转义:八进制转义在 u 模式下是非法的。/\0/ 匹配 U+0000

这就是你浏览器的引擎

模式会直接交给运行时的 RegExp 构造函数,所以你看到的就是 Node 和浏览器会做的事。Python 的 re、PCRE、Go 的 RE2 和 grep 都有所不同:有时是语法不同,有时是相同语法的含义不同。

\d 在这里是 [0-9],但并非处处如此

无论是否开启 u 标志,JavaScript 都让 \d、\w 和 \b 保持在 ASCII 范围内。Python 和 .NET 默认把 \d 扩展到所有 Unicode 十进制数字,所以 ٢٠٢٦ 在那里能通过,在这里会失败。想要宽泛的含义时,请使用 \p{Nd}。

嵌套超出了能力范围

没有任何正则表达式能匹配平衡括号,所以无论模式写得多长,HTML、JSON 和源代码都无法处理。这里的 HTML 预设特意标注为快速浏览用途,而不是解析器。

本页的一切都在你的浏览器中运行,使用的是浏览器自带的 RegExp 实现:任何模式、测试文本和结果都不会被发送到任何地方。保护限制明确写出,而不是暗中设定:每次运行最多处理 100,000 个字符的测试文本、5,000 个匹配和 250 ms 的实际耗时,超出后运行会停止并给出提示。零长度匹配总会让光标前进一个完整的码点,所以像 /a*/g 这样的模式会正常结束,而不会陷入循环。

工作原理

一个模式,一段文本,以及引擎对这一组合所知道的一切。

正则表达式是一个微型程序,它之所以难以调试,是因为运行过程不可见:你只看到答案,从来看不到过程。本页把过程变得可见。你的模式由浏览器自带的 RegExp 构造函数编译,在长度、匹配数量和实际耗时的上限内对你的文本运行,然后逐个报告匹配结果:每个匹配从哪里开始、到哪里结束,每个捕获组包含什么,哪些分组完全没有参与,以及一次替换会得到什么。因为这是运行时自己的引擎而不是重新实现,所以结果就是你的 JavaScript 会得到的结果。坦白说,它们并不是 Python、PCRE、Go 的 RE2 或 grep 会得到的结果。

  1. 01

    写下模式并选择标志

    输入模式时不用写两侧的斜杠,也可以粘贴完整的字面量,例如 /\d+/gi,标志会一并带入。七个标志都是带标签的开关,而不是需要记住的字母;出现语法错误时,会用一句话说明,而不是照搬运行时的措辞。

  2. 02

    粘贴你真正要处理的文本

    使用真实输入,而不是刻意构造的例子:一段日志、一列 CSV、一页文章。输入时匹配项会实时高亮,并交替使用深浅两种底色,让两个紧挨着的匹配仍然是两个匹配;零宽匹配会画成一个标记,而不是直接消失。

  3. 03

    查看分组,再试试替换

    每个匹配都会列出偏移量以及每个编号分组和命名分组,包括没有参与匹配的分组。切换到“替换”可预览展开了 $1、$<name>、$& 等写法的替换结果,切换到“拆分”可查看 String.split 用同一个模式会得到什么。

为调试而生,而不是为了演示

其他测试工具含糊带过的部分:空匹配、缺席的分组,以及难以驯服的模式。

匹配过程不会让标签页卡死

零长度匹配会让光标前进一个完整的码点,所以 /a*/g 会正常结束,而不是无限循环。实际耗时预算会在运行开始变慢时将其停止,并提示该模式回溯严重,而不是卡在一个永远转不完的加载图标后面。

未参与匹配的分组依然显示

用 /(a)|(b)/ 匹配“b”时,第 1 组为 undefined,第 2 组包含匹配内容。丢掉空位的测试工具会悄悄把后面的分组全部重新编号。这里会保留这个位置,并标注为未参与匹配,而这正是会让下游代码出错的区别。

每个分组都有偏移量,而不仅仅是整个匹配

打开 d 标志后,每个捕获都会报告自己的起止索引,数据来自 match.indices。没有它,你只能得到整个匹配的位置,必须重新搜索分组的文本,而一旦同样的文本出现两次就会出错。

能展开棘手情况的替换预览

$1、$<name>、$&、$`、$' 和 $$ 在这里都会被实际展开,而不只是给出描述,包括这条规则:第 12 组存在时,$12 表示第 12 组;不存在时,则表示第 1 组后面跟一个“2”。输出结果会与 String.replace 的结果进行比对。

把语法错误翻译成可操作的提示

“Invalid regular expression: /(/: Unterminated group”会变成“打开了一个 ( 但从未关闭”,并附带说明:字面意义的括号需要转义。V8、JavaScriptCore 和 SpiderMonkey 各自的措辞都能识别,共涵盖约十五类错误。

十个预设,如实说明能匹配什么

电子邮件模式被描述为能发现拼写错误的实用写法,而不是 RFC 5322 校验。IPv4 模式把每个八位组限制在 255 以内,并坦言它仍会在版本号字符串中找到一个地址。每个预设都附带示例文本,其中包含它会拒绝的情况。

正则常见问题

贪婪与惰性、灾难性回溯,以及 JavaScript 与 PCRE 的分歧。

这里测试的是哪种正则风格?+

就是你浏览器中已经在运行的那一种。本页没有打包任何引擎:你的模式会直接交给 JavaScript 运行时自带的 RegExp 构造函数,你看到的匹配就是你的代码会得到的匹配。因此结果对 JavaScript、TypeScript、Node、Deno 和 Bun 完全准确,对其他环境则只是近似。Python 的 re、PHP 和 Perl 中的 PCRE、Go 的 RE2、Rust 的 regex crate、.NET、Java、grep、sed 和 ripgrep 都有差异,有时在语法上,有时在同一语法的含义上。如果模式要用在其他语言中,上线前请在那里再测试一遍。

贪婪量词和惰性量词有什么区别?+

贪婪量词会尽可能多地匹配,然后一次退回一个字符,直到模式的其余部分能够匹配。惰性量词(在后面加一个问号,即 *? +? ?? {n,m}?)会尽可能少地匹配,只有在不得不时才扩展。经典示例是用 /<.+>/ 匹配“<a><b>”:贪婪的 .+ 会一路吞到末尾,再回溯到最后一个“>”,把“<a><b>”作为一个整体匹配返回。改成 /<.+?>/,你会依次得到“<a>”和“<b>”。两者没有谁更正确,它们回答的是不同的问题。实用的规则是:分隔符唯一时,默认用贪婪;分隔符会重复时,默认用惰性。

什么是灾难性回溯?+

当一个模式可以用许多种方式切分同一段文本,而最终匹配又失败时,就会发生灾难性回溯。JavaScript 通过回溯进行匹配,所以失败时会把剩下的每一种组合都试一遍才放弃。教科书式的诱因是重复里面套重复,例如用 /(a+)+$/ 匹配一长串“a”后面跟一个“b”。把 30 个 a 分成若干个至少含一个字符的组,方法数量呈指数级增长,引擎会全部试完才断定“$”无法匹配。三十个字符只需几毫秒;四十个字符大约要慢一千倍。解决办法包括:让内外两层重复无法匹配相同的文本,用字符类替换带量词的分组,或者给模式加上锚点以便尽早发现失败。本页会在运行前扫描你的模式中是否存在这些结构,并指出它们。

为什么后行断言这么晚才进入 JavaScript?+

先行断言从一开始就存在于语言中,但后行断言直到 ES2018 才加入(晚了二十多年),而在 Safari 于 2023 年支持之前,使用它的模式会在一个许多网站仍需支持的浏览器上抛出语法错误。这一延迟部分源于从 ES4 被放弃到 ES6 之间语言演进的整体放缓,部分则因为后行断言确实更难实现:引擎必须从当前位置向后匹配。JavaScript 的实现最终比大多数引擎都好,因为与 PCRE、Python 的 re 和 Java 不同,它允许变长后行断言。/(?<=\$\d+ )item/ 在这里是合法的,在那些引擎里则会被直接拒绝。如果你的模式需要在旧环境中运行,传统的变通办法是用一个分组捕获前面的上下文,事后再丢弃。

u 标志和 v 标志有什么区别?+

u 标志来自 ES2015,会让模式进入 Unicode 模式:emoji 等辅助平面字符算作一个单位,而不是两个 UTF-16 半部分;\u{1F600} 变为合法;\p{…} 属性转义被启用;没有意义的转义会变成语法错误,而不是被悄悄忽略。v 标志来自 ES2024,是 u 的超集,在字符类中加入了集合表示法(用 -- 求差集,用 && 求交集,以及多字符字符串属性),因此你可以写 [\p{Letter}--[a-z]] 来表示“除 ASCII 小写字母外的任意字母”。这两个标志互斥:一个正则要么是 u 模式,要么是 v 模式,不可能两者兼有。本页提供 u 而不提供 v,因为一对会悄悄互相抵消的按钮,不如一个如实说明自身范围的界面。

为什么我的全局正则每隔一个匹配就跳过一个?+

因为带有 g 或 y 标志的正则是有状态的。它保存一个 lastIndex 属性,exec 和 test 都从那里开始,并在成功时更新它。把这样一个正则存进模块级常量,对同一个字符串调用两次 test,第二次就会从中途开始并返回 false。在循环中对多个条目复用全局正则时,同样的问题也会出现。解决办法有三种:在使用处重新创建正则,每次调用前把 lastIndex 重置为 0,或者改用 matchAll 和 String.match,它们不会把光标留在可能绊倒你的地方。本页每次运行都会编译一个私有的工作副本,从而彻底绕开这个问题,所以你看到的正则永远不是被修改的那一个。

PCRE 能做到哪些 JavaScript 正则做不到的事?+

有好几项,知道是哪些,可以在你跨引擎复制模式时省去大量令人困惑的调试。没有原子组:PCRE 的 (?>…) 会锁定一次匹配并拒绝回溯进去,这是手动修复灾难性回溯的标准做法,而 JavaScript 唯一的替代方案是用先行断言包住一个捕获。出于同样的原因,也没有占有量词(a*+)。没有递归 (?R),也没有子程序调用,所以平衡括号无法匹配。没有内联修饰符:在模式中间写 (?i) 是语法错误,因为 JavaScript 的标志要么作用于整个正则,要么完全不起作用。没有条件分支,没有 \A \z \Z 锚点,没有 [[:alpha:]] 这样的 POSIX 字符类,也没有注释模式。而 JavaScript 拥有、许多引擎却没有的,是变长后行断言。

\d 在所有地方含义都一样吗?+

不一样,而且这种差异是安全漏洞的真实来源。在 JavaScript 中,\d 永远恰好等于 [0-9],无论有没有 u 标志。在 .NET 和 Python 3 中,\d 默认匹配任何具有 Unicode 十进制数字属性的字符,包括阿拉伯-印度数字 ٠١٢、天城文数字和全角数字 012。一个用 Python 编写、接受 ٢٠٢٦ 的校验器,和一个用 JavaScript 编写、拒绝它的校验器,会对同一输入得出不同结论,而这正是会被利用的那种不一致。同样的提醒也适用于 \w,它在这里是 [A-Za-z0-9_],因此不把“é”视为单词字符;也适用于 \b,它是根据 \w 定义的,所以会落在“naïve”的中间。如果你想在 JavaScript 中使用 Unicode 语义,必须显式要求:数字用带 u 标志的 \p{Nd},字母用 \p{L}。

什么时候应该停止使用正则表达式?+

当你要匹配的东西可以嵌套时。形式意义上的正则表达式不会计数,虽然反向引用让 JavaScript 的引擎超出了严格的正则语言,但它仍然无法匹配平衡结构。这就排除了 HTML、XML、JSON、源代码以及任何带括号的语言,不是因为模式难写,而是因为根本不存在这样的模式。请使用 DOMParser、JSON.parse 或真正的解析器。还有三个较温和的信号指向同一方向:模式已经超过一两行,没人能读懂;每个子句都需要注释来解释;或者它不断为本应早已处理好的输入添加特殊情况。正则表达式最擅长查找和校验扁平、结构规整的片段(日期、十六进制颜色、日志前缀),最不擅长的是假装自己是一套语法。

替换中的 $1、$&、$` 和 $' 是什么意思?+

它们是 String.replace 在替换字符串中能识别的替换模式。$1 到 $99 插入对应编号捕获组的文本,$<name> 对命名组做同样的事。$& 插入整个匹配。$` 插入匹配之前的所有内容,$' 插入匹配之后的所有内容;两者在包裹文本时出奇地有用,也出奇地容易被意外触发,因为反斜杠无法转义它们。$$ 插入一个字面意义的美元符号。有两条规则常让人措手不及:引用不存在的分组时,不会抛出错误,而是作为普通文本留在输出中;如果模式有十二个分组,$12 表示第 12 组,否则表示第 1 组后跟字符“2”。本页会在预览中展开所有这些写法,并标出无法解析的引用。

更多专注好用的工具,随时待命。

探索不断扩充的工具合集,涵盖计算、文档、写作和日常工作。

浏览全部工具