快速入门
教程
工具和语言
示例
参考
书评
正则表达式教程
简介
目录
特殊字符
不可打印字符
正则表达式引擎内部
字符类
字符类减法
字符类交集
简写字符类
锚点
词边界
交替
可选项
重复
分组和捕获
反向引用
反向引用,第 2 部分
命名组
相对反向引用
分支重置组
自由间距和注释
Unicode
模式修饰符
原子分组
独占量词
前瞻和后顾
环视,第 2 部分
将文本保留在匹配项之外
条件
平衡组
递归
子例程
无限递归
递归和量词
递归和捕获
递归和反向引用
递归和回溯
POSIX 方括号表达式
零长度匹配
继续匹配
本网站上的更多内容
简介
正则表达式快速入门
正则表达式教程
替换字符串教程
应用程序和语言
正则表达式示例
正则表达式参考
替换字符串参考
书评
可打印 PDF
关于本网站
RSS 提要和博客
RegexBuddy—Better than a regular expression tutorial!

使用平衡组匹配嵌套结构

.NET 正则表达式风格有一个称为平衡组的特殊功能。平衡组的主要目的是匹配平衡结构或嵌套结构,这也是它们名称的由来。从技术上来说,该功能更准确的名称应该是捕获组减法。这就是该功能的实际作用。这是 .NET 为其他正则表达式风格(如 PerlPCRERuby)处理 正则表达式递归 的问题而提供的解决方案。JGsoft V2 同时支持平衡组和递归。

(?<capture-subtract>regex)(?'capture-subtract'regex) 是平衡组的基本语法。它与 .NET 中 命名的捕获组 使用的语法相同,但组名之间用减号分隔。此组的名称为“capture”。您可以省略组的名称。 (?<-subtract>regex)(?'-subtract'regex) 是非捕获平衡组的语法。

名称“subtract”必须是正则表达式中另一个组的名称。当正则表达式引擎进入平衡组时,它会从组“subtract”中减去一个匹配项。如果组“subtract”尚未匹配,或者其所有匹配项均已减去,则平衡组将无法匹配。您可以将平衡组视为 条件,它测试组“subtract”,其中“regex”作为“if”部分,而“else”部分始终无法匹配。不同之处在于,平衡组具有从组“subtract”中减去一个匹配项的附加功能,而条件则不会更改组。

如果平衡组成功并且它具有名称(在此示例中为“capture”),则该组将捕获从从组“subtract”中减去的匹配项的结束和平衡组本身的匹配项的开始之间的文本(在此示例中为“regex”)。

此功能在 .NET 中起作用的原因是 .NET 中的捕获组会保留在匹配过程中捕获的所有内容的堆栈,而这些内容不会回溯或减去。大多数其他正则表达式引擎只存储每个捕获组的最新匹配项。当 (\w)+ 匹配 abc 时,Match.Groups[1].Value 会返回 c,就像其他正则表达式引擎一样,但 Match.Groups[1].Captures 会存储该组的所有三个迭代:abc

查看正则表达式引擎内部

让我们将正则表达式 (?'open'o)+(?'between-open'c)+ 应用于字符串 ooccc(?'open'o) 匹配第一个 o,并将其存储为组“open”的第一个捕获。 量词 + 重复该组。 (?'open'o) 匹配第二个 o,并将其存储为第二个捕获。再次重复,(?'open'o) 无法匹配第一个 c。但 + 对两次重复感到满意。

正则表达式引擎前进到 (?'between-open'c)。在引擎进入此平衡组之前,它必须检查已减去的组“open”是否捕获了某些内容。它捕获了第二个 o。引擎进入组,从“open”减去最近的捕获。这使得组“open”仅捕获第一个 o。现在在平衡组内部,c 匹配 c。引擎退出平衡组。组“between”捕获了从“open”减去的匹配(第二个 o)和平衡组刚刚匹配的 c 之间的文本。这是一个空字符串,但无论如何它都会被捕获。

平衡组也有 + 作为其量词。引擎再次发现已减去的组“open”捕获了某些内容,即第一个 o。正则表达式进入平衡组,使组“open”没有任何匹配项。c 匹配字符串中的第二个 c。组“between”捕获 oc,它是从“open”减去的匹配(第一个 o)和平衡组刚刚匹配的第二个 c 之间的文本。

平衡组再次重复。但这一次,正则表达式引擎发现组“open”没有剩余的匹配项。平衡组匹配失败。组“between”不受影响,保留其最近的捕获。

+ 对两次迭代感到满意。引擎已到达正则表达式的末尾。它返回 oocc 作为整体匹配。 Match.Groups['open'].Success 将返回 false,因为该组的所有捕获都被减去了。 Match.Groups['between'].Value 返回 "oc"

匹配平衡对

如果我们希望匹配平衡的 o 和 c,我们需要修改此正则表达式。为了确保正则表达式不会匹配 ooccc(其中 c 多于 o),我们可以添加 锚点^(?'open'o)+(?'-open'c)+$。此正则表达式经历与前一个正则表达式相同的匹配过程。但是,在 (?'-open'c)+ 无法匹配其第三次迭代后,引擎到达 $,而不是正则表达式的结尾。这无法匹配。正则表达式引擎将回溯,尝试量词的不同排列,但它们都无法匹配。找不到匹配项。

但是正则表达式 ^(?'open'o)+(?'-open'c)+$ 仍然匹配 ooc。匹配过程再次相同,直到平衡组匹配第一个 c,并将组“open”留给第一个 o 作为其唯一的捕获。量词使引擎再次尝试平衡组。引擎再次发现减去的组“open”捕获了某些内容。正则表达式进入平衡组,将组“open”留给没有任何匹配项。但是现在,c 无法匹配,因为正则表达式引擎已到达字符串的末尾。

正则表达式引擎现在必须回溯出平衡组。在回溯平衡组时,.NET 也会回溯减法。由于在进入平衡组时第一个 o 的捕获从“open”中减去,因此在回溯出平衡组时,此捕获现在已还原。重复组 (?'-open'c)+ 现在减少到一次迭代。但是量词对此没有问题,因为 + 始终表示“一次或多次”。仍然在字符串的末尾,正则表达式引擎到达正则表达式中的 $,它匹配。整个字符串 ooc 作为整体匹配项返回。 Match.Groups['open'].Captures 将字符串中的第一个 o 作为 CaptureCollection 中的唯一项保存。这是因为,在回溯后,第二个 o 从组中减去,但第一个 o 没有。

为了确保正则表达式匹配 ocoocc 但不匹配 ooc,我们需要检查组“open”在匹配过程到达正则表达式的末尾时没有剩余捕获。我们可以使用 条件 来实现此目的。(?(open)(?!)) 是一个条件,用于检查组“open”是否匹配某些内容。在 .NET 中,匹配某些内容意味着在堆栈上仍然有未回溯或未减去的捕获。如果组已捕获某些内容,则评估条件的“if”部分。在这种情况下,这是空负向前查找 (?!)。此向前查找中的空字符串始终匹配。由于向前查找为负,因此这会导致向前查找始终失败。因此,如果组已捕获某些内容,则条件始终失败。如果组未捕获任何内容,则评估条件的“else”部分。在这种情况下,没有“else”部分。这意味着如果组未捕获任何内容,则条件始终成功。这使得 (?(open)(?!)) 成为验证组“open”没有剩余捕获的适当测试。

正则表达式 ^(?'open'o)+(?'-open'c)+(?(open)(?!))$ 无法匹配 ooc。当 c 无法匹配,因为正则表达式引擎已到达字符串末尾,引擎将回溯出平衡组,使“open”具有单个捕获。正则表达式引擎现在到达条件,无法匹配。正则表达式引擎将回溯尝试量词的不同排列,但它们都无法匹配。找不到匹配项。

正则表达式 ^(?'open'o)+(?'-open'c)+(?(open)(?!))$ 确实匹配 oocc。在 (?'-open'c)+ 匹配 cc 后,正则表达式引擎无法第三次进入平衡组,因为“open”没有剩余的捕获。引擎前进到条件。条件成功,因为“open”没有剩余的捕获,并且条件没有“else”部分。现在 $ 在字符串末尾匹配。

匹配平衡结构

^(?:(?'open'o)+(?'-open'c)+)+(?(open)(?!))$ 将捕获组和平衡组包装在 非捕获组 中,该组也会重复。此正则表达式匹配任何类似 ooocooccocccoc 的字符串,其中包含任意数量的完全平衡的 o 和 c,以及任意数量的成对序列,嵌套到任意深度。平衡组确保正则表达式永远不会匹配在字符串中的任何点处 c 比其左侧的 o 多的字符串。位于重复组外部的末尾条件确保正则表达式永远不会匹配 o 比 c 多的字符串。

^(?>(?'open'o)+(?'-open'c)+)+(?(open)(?!))$ 通过使用原子组而不是非捕获组来优化之前的正则表达式。原子组(也是非捕获的)消除了几乎所有正则表达式无法找到匹配项时的回溯,当在包含大量 o 和 c 但在结尾处未正确平衡的长字符串上使用时,这可以极大地提高性能。原子组不会改变正则表达式匹配具有平衡 o 和 c 的字符串的方式。

^m*(?>(?>(?'open'o)m*)+(?>(?'-open'c)m*)+)+(?(open)(?!))$ 允许字符串中的任何位置出现任意数量的字母 m,同时仍然要求所有 o 和 c 保持平衡。m* 在正则表达式的开头允许在第一个 o 之前出现任意数量的 m。(?'open'o)+ 已更改为 (?>(?'open'o)m*)+ 以允许每个 o 后出现任意数量的 m。类似地,(?'-open'c)+ 已更改为 (?>(?'-open'c)m*)+ 以允许每个 c 后出现任意数量的 m。

这是使用 .NET 平衡组或捕获组减法特性匹配平衡结构的通用解决方案。您可以用任何正则表达式替换 omc,只要这三个表达式中的任意两个不能匹配相同的文本即可。

^[^()]*(?>(?>(?'open'\()[^()]*)+(?>(?'-open'\))[^()]*)+)+(?(open)(?!))$ 将此技术应用于匹配其中所有括号都完美平衡的字符串。

对减法组的回引

您可以对被平衡组减去的组使用 回引。回引匹配组最近一次未回溯或减去的匹配。正则表达式 (?'x'[ab]){2}(?'-x')\k'x' 匹配 aaaabababbbb。它不匹配 aababbbaabba。字符串的第一个和第三个字母必须相同。

让我们看看 (?'x'[ab]){2}(?'-x')\k'x' 如何匹配 aba(?'x'[ab]) 的第一次迭代捕获 a。第二次迭代捕获 b。现在,正则表达式引擎到达平衡组 (?'-x')。它检查组“x”是否已匹配,它已匹配。引擎进入平衡组,从组“x”的堆栈中减去匹配 b。平衡组内没有正则表达式标记。它匹配而不向前推进字符串。现在,正则表达式引擎到达反向引用 \k'x'。组“x”堆栈顶部的匹配是 a。字符串中的下一个字符也是反向引用匹配的 aaba 被发现为整体匹配。

当你将此正则表达式应用于 abb 时,匹配过程是相同的,除了反向引用无法匹配字符串中的第二个 b。由于正则表达式没有正则表达式引擎可以尝试的其他排列,因此匹配尝试失败。

匹配回文

^(?'letter'[a-z])+[a-z]?(?:\k'letter'(?'-letter'))+(?(letter)(?!))$ 匹配任何长度的回文词。此正则表达式利用了反向引用和捕获组减法协同工作的事实。它还使用空平衡组作为上一节中的正则表达式。

让我们看看此正则表达式如何匹配回文 radar^ 匹配字符串的开头。然后 (?'letter'[a-z])+ 迭代五次。组“letter”最终在其堆栈上匹配五次:radar。正则表达式引擎现在位于字符串的末尾和正则表达式中的 [a-z]?。它不匹配,但没关系,因为量词使其可选。引擎现在到达反向引用 \k'letter'。组“letter”在其堆栈顶部有 r。这无法匹配字符串末尾后的空隙。

正则表达式引擎回溯。(?'letter'[a-z])+ 减少到四次迭代,在组“letter”的堆栈中留下 rada[a-z]? 匹配 r。反向引用再次无法匹配字符串末尾后的空值。引擎回溯,强制 [a-z]? 放弃其匹配。现在,“letter” 的堆栈顶部有 a。这会导致反向引用无法匹配 r

接下来是更多的回溯。(?'letter'[a-z])+ 减少到三次迭代,在组“letter”的堆栈顶部留下 d。引擎再次使用 [a-z]?。它再次失败,因为没有 d 与反向引用匹配。

再次回溯,组“letter”的捕获堆栈减少到 ra。现在形势逆转。[a-z]? 匹配 d。反向引用匹配 a,这是组“letter”最近一次未回溯的匹配。引擎现在到达空平衡组 (?'-letter')。这匹配,因为组“letter”有一个匹配 a 要减去。

反向引用和平衡组位于一个重复的非捕获组内,因此引擎再次尝试它们。反向引用匹配 r,平衡组从“letter”的堆栈中减去它,使捕获组没有任何匹配。再次迭代,反向引用失败,因为组“letter”的堆栈上没有匹配项。这使得该组充当一个不参与组。在 .NET 中,对不参与组的反向引用总是失败,就像在大多数正则表达式风格中一样。

(?:\k'letter'(?'-letter'))+ 已成功匹配两次迭代。现在,条件 (?(letter)(?!)) 成功,因为组“letter”没有匹配项。锚 $ 也匹配。回文 radar 已匹配。