匹配过程不会让标签页卡死
零长度匹配会让光标前进一个完整的码点,所以 /a*/g 会正常结束,而不是无限循环。实际耗时预算会在运行开始变慢时将其停止,并提示该模式回溯严重,而不是卡在一个永远转不完的加载图标后面。
写下模式,粘贴你真正要处理的文本,输入时每个匹配项都会实时亮起,同时显示捕获组、命名组、偏移量和替换预览,并用通俗易懂的语言解释语法错误,而不是照搬运行时自己的措辞。
3 个匹配,3 个捕获组。第一个匹配位于索引 9。
也可以粘贴完整的字面量,例如 /\d+/gi,标志会一并带入。下方所有内容都会在每次按键时重新计算。
编译成功。3 个捕获组(year、month、day)· 模式长度为 70 个字符。
/\b(?<year>\d{4})-(?<month>0[1-9]|1[0-2])-(?<day>0[1-9]|[12]\d|3[01])\b/g 已在 114 个字符上运行。
匹配项会在输入时着色。相邻匹配交替使用深浅底色,每个匹配的左边缘还有一条竖线,所以两个紧挨着的匹配不会被看成一个。
114 / 100,000 个字符 · 4 行
每个匹配及其偏移量和捕获组。
打开 d 标志,即可看到每个分组的位置,而不只是它的内容。
每个预设都会载入它的模式、标志、替换内容,以及一段示例文本,其中包含它刻意拒绝的情况。
带锚点的模式,检查整行是否符合格式。
不带锚点的模式,从连续的正文或日志中提取条目。
重点在于替换而不是搜索的模式。
专门针对 JavaScript 语法:注释会指出它与 PCRE 和 Python 不同的地方。
文本中的一个位置,以及允许填入该位置的字符集合。
前面的内容可以重复多少次。默认是贪婪的;加上 ? 即可变为惰性。
零宽断言。它们不消耗任何字符,只判断位置。
捕获能把一次测试变成提取出的数据,但在你不需要时也会白白占用内存。
检查当前位置周围的文本但不消耗它的断言。
作用于整个正则。JavaScript 没有内联的 (?i) 修饰符:标志要么处处生效,要么完全不生效。
有十二个字符是特殊字符。要按字面匹配其中一个,请在它前面加反斜杠。
模式会直接交给运行时的 RegExp 构造函数,所以你看到的就是 Node 和浏览器会做的事。Python 的 re、PCRE、Go 的 RE2 和 grep 都有所不同:有时是语法不同,有时是相同语法的含义不同。
无论是否开启 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 会得到的结果。
输入模式时不用写两侧的斜杠,也可以粘贴完整的字面量,例如 /\d+/gi,标志会一并带入。七个标志都是带标签的开关,而不是需要记住的字母;出现语法错误时,会用一句话说明,而不是照搬运行时的措辞。
使用真实输入,而不是刻意构造的例子:一段日志、一列 CSV、一页文章。输入时匹配项会实时高亮,并交替使用深浅两种底色,让两个紧挨着的匹配仍然是两个匹配;零宽匹配会画成一个标记,而不是直接消失。
每个匹配都会列出偏移量以及每个编号分组和命名分组,包括没有参与匹配的分组。切换到“替换”可预览展开了 $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 运行时自带的 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 分成若干个至少含一个字符的组,方法数量呈指数级增长,引擎会全部试完才断定“$”无法匹配。三十个字符只需几毫秒;四十个字符大约要慢一千倍。解决办法包括:让内外两层重复无法匹配相同的文本,用字符类替换带量词的分组,或者给模式加上锚点以便尽早发现失败。本页会在运行前扫描你的模式中是否存在这些结构,并指出它们。
先行断言从一开始就存在于语言中,但后行断言直到 ES2018 才加入(晚了二十多年),而在 Safari 于 2023 年支持之前,使用它的模式会在一个许多网站仍需支持的浏览器上抛出语法错误。这一延迟部分源于从 ES4 被放弃到 ES6 之间语言演进的整体放缓,部分则因为后行断言确实更难实现:引擎必须从当前位置向后匹配。JavaScript 的实现最终比大多数引擎都好,因为与 PCRE、Python 的 re 和 Java 不同,它允许变长后行断言。/(?<=\$\d+ )item/ 在这里是合法的,在那些引擎里则会被直接拒绝。如果你的模式需要在旧环境中运行,传统的变通办法是用一个分组捕获前面的上下文,事后再丢弃。
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 唯一的替代方案是用先行断言包住一个捕获。出于同样的原因,也没有占有量词(a*+)。没有递归 (?R),也没有子程序调用,所以平衡括号无法匹配。没有内联修饰符:在模式中间写 (?i) 是语法错误,因为 JavaScript 的标志要么作用于整个正则,要么完全不起作用。没有条件分支,没有 \A \z \Z 锚点,没有 [[:alpha:]] 这样的 POSIX 字符类,也没有注释模式。而 JavaScript 拥有、许多引擎却没有的,是变长后行断言。
不一样,而且这种差异是安全漏洞的真实来源。在 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 或真正的解析器。还有三个较温和的信号指向同一方向:模式已经超过一两行,没人能读懂;每个子句都需要注释来解释;或者它不断为本应早已处理好的输入添加特殊情况。正则表达式最擅长查找和校验扁平、结构规整的片段(日期、十六进制颜色、日志前缀),最不擅长的是假装自己是一套语法。
它们是 String.replace 在替换字符串中能识别的替换模式。$1 到 $99 插入对应编号捕获组的文本,$<name> 对命名组做同样的事。$& 插入整个匹配。$` 插入匹配之前的所有内容,$' 插入匹配之后的所有内容;两者在包裹文本时出奇地有用,也出奇地容易被意外触发,因为反斜杠无法转义它们。$$ 插入一个字面意义的美元符号。有两条规则常让人措手不及:引用不存在的分组时,不会抛出错误,而是作为普通文本留在输出中;如果模式有十二个分组,$12 表示第 12 组,否则表示第 1 组后跟字符“2”。本页会在预览中展开所有这些写法,并标出无法解析的引用。
探索不断扩充的工具合集,涵盖计算、文档、写作和日常工作。