正则基础之——平衡组

1          概述

平衡组是微软在 .NET 中提出的一个概念,主要是结合几种正则语法规则,提供对配对出现的嵌套结构的匹配。 .NET 是目前对正则支持最完备、功能最强大的语言平台之一,而平衡组正是其强大功能的外在表现,也是比较实用的文本处理功能,目前只有 .NET 支持,相信后续其它语言会提供支持。

平衡组可以有狭义和广义两种定义,狭义平衡组指 .NET 中定义的 (?<Close-Open>Expression) 语法,广义平衡组并不是固定的语法规则,而是几种语法规则的综合运用,我们平时所说的平衡组通常指的是广义平衡组。本文中如无特殊说明,平衡组这种简写指的是广义平衡组。

正是由于平衡组功能的强大,所以带来了一些神秘色彩,其实平衡组并不难掌握。下面就平衡组的匹配原理、应用场景以及性能调优展开讨论。

2        平衡组匹配原理

2.1      预备知识

平衡组通常是由量词,分支结构,命名捕获组,狭义平衡组,条件判断结构组成的,量词和分支结构这里不做介绍,这里只对命名捕获组,狭义平衡组和条件判断结构做下说明。

2.1.1   命名捕获组

语法: (?<name>Expression)  

  (?’name’Expression)

以上两种写法在 .NET 中是等价的,都是将“ Expression ”子表达式匹配到的内容,保存到以“ name ”命名的组里,以供后续引用。

对于命名捕获组的应用,这里不做重点介绍,只是需要澄清一点,平时使用捕获组时,一般反向引用或 Group 对象使用得比较多,可能会有一种误解,那就是捕获组只保留一个匹配结果,即使一个捕获组可以先后匹配多个子串,也只保留最后一个匹配到的子串。但事实是这样吗?

举例来说:

源字符串: abcdefghijkl

正则表达式: (?<chars>[a-z]{2})+

命名捕获组 chars 最终捕获的是什么?

string test = "abcdefghijkl" ;

Regex reg = new Regex (@"(?<chars>[a-z]{2})+" );

Match m = reg.Match(test);

if (m.Success)

{

      richTextBox2.Text += " 匹配结果:" + m.Value + "/n" ;

      richTextBox2.Text += "Group " + m.Groups["chars" ].Value + "/n" ;

}

/*-------- 输出--------

匹配结果:abcdefghijkl

Group kl

*/

m.Groups["chars"].Value 的输出上看,似乎确实是只保留了一个匹配内容,但却忽略了一个事实, Group 实际上是 Capture 的一个集合

string test = "abcdefghijkl" ;

Regex reg = new Regex (@"(?<chars>[a-z]{2})+" );

Match m = reg.Match(test);

if (m.Success)

{

     richTextBox2.Text += " 匹配结果:" + m.Value + "/n" ;

     richTextBox2.Text += "Group " + m.Groups["chars" ].Value + "/n--------------/n" ;

     foreach (Capture c in m.Groups["chars" ].Captures)

     {

           richTextBox2.Text += "Capture " + c + "/n" ;

     }

}

/*-------- 输出--------

匹配结果:abcdefghijkl

Group kl

--------------

Capture ab

Capture cd

Capture ef

Capture gh

Capture ij

Capture kl

*/

平时应用时可能会忽略这一点,因为很少遇到一个捕获组先后匹配多个子串的情况,而在一个捕获组只匹配一个子串时, Group 集合中就只有一个 Capture 元素,所以内容是一样的。

string test = "abcdefghijkl" ;

Regex reg = new Regex (@"(?<chars>[a-z]{2})" );

Match m = reg.Match(test);

if (m.Success)

{

     richTextBox2.Text += " 匹配结果:" + m.Value + "/n" ;

     richTextBox2.Text += "Group " + m.Groups["chars" ].Value + "/n--------------/n" ;

     foreach (Capture c in m.Groups["chars" ].Captures)

     {

          richTextBox2.Text += "Capture " + c + "/n" ;

     }

}

/*-------- 输出--------

匹配结果:ab

Group ab

--------------

Capture ab

*/

捕获组保存的是一个集合,而不只是一个元素,这一知识点对于理解平衡组的匹配原理是有帮助的。

2.1.2   狭义平衡组

语法: (?<Close-Open>Expression)

其中“ Close ”是命名捕获组的组名,也就是“ (?<name>Expression) ”中的“ name ”,可以省略,通常应用时并不关注,所以一般都是省略的,写作“ (?<-Open>Expression) ”。作用就是当此处的“ Expression ”子表达式匹配成功时,则将最近匹配成功到的命名为“ Open ”组出栈,如果此前不存在匹配成功的“ Open ”组,那么就报告“ (?<-Open>Expression) ”匹配失败,整个表达式在这一位置也是匹配失败的。

2.1.3   条件判断结构

语法: (?(Expression)yes|no)

      (?(name)yes|no)

对于“ (?(Expression)yes|no) ”,它是“ (?(?=Expression)yes|no) ”的简写形式,相当于三元运算符

(?=Expression) ? yes : no

表示如果子表达式“ (?=Expression) ”匹配成功,则匹配“ yes ”子表达式,否则匹配“ no ”子表达式。如果“ Expression ”与可能出现的命名捕获组的组名相同,为避免混淆,可以采用“ (?(?=Expression)yes|no) ”方式显示声明“ Expression ”为子表达式,而不是捕获组名。

(?=Expression) ”验证当前位置右侧是否能够匹配“ Expression ”,属于顺序环视结构,是零宽度的,所以它只参与判断,即使匹配成功,也不会占有字符。

举例来说:

源字符串: abc

正则表达式: (?(?=a)/w{2}|/w)

当前位置右侧如果是字符“ a ,则匹配两个“ /w ”,否则匹配一个“ /w ”。

string test = "abc" ;

Regex reg = new Regex (@"(?(?=a)/w{2}|/w)" );

MatchCollection mc = reg.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "/n" ;

}

/*-------- 输出--------

ab

c

*/

对于“ (?(name)yes|no) ”,如果命名捕获组“ name ”有捕获,则匹配“ yes ”子表达式,否则匹配“ no ”子表达式。这一语法最典型的一种应用是平衡组。

当然,以上两种语法中,“ yes ”和“ no 都是可以省略的,但同一时间只能省略一个,不能一起省略。平衡组的应用中就是省略了“ no ”子表达式。

2.2      平衡组的匹配原理

平衡组的匹配原理可以用堆栈来解释,先举个例子,再根据例子进行解释。

源字符串: a+(b*(c+d))/e+f-(g/(h-i))*j

正则表达式: /(((?<Open>/()|(?<-Open>/))|[^()])*(?(Open)(?!))/)

需求说明:匹配成对出现的 () 中的内容

string test = "a+(b*(c+d))/e+f-(g/(h-i))*j" ;

Regex reg = new Regex (@"/(((?<Open>/()|(?<-Open>/))|[^()])*(?(Open)(?!))/)" );

MatchCollection mc = reg.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "/n" ;

}

/*-------- 输出--------

(b*(c+d))

(g/(h-i))

*/

下面来考察一下这个正则,为了阅读方便,写成宽松模式。

Regex reg = new Regex (@"/(                          # 普通字符 (

                            (                       # 分组构造,用来限定量词 * 修饰范围

                                (?<Open>/()         # 命名捕获组,遇到开括弧 Open 计数加1

                            |                       # 分支结构

                                  (?<-Open>/))        # 狭义平衡组,遇到闭括弧 Open 计数减1

                            |                        # 分支结构

                                  [^()]+              # 非括弧的其它任意字符

                            )*                      # 以上子串出现0 次或任意多次

                            (?(Open)(?!))           # 判断是否还有 Open ,有则说明不配对,什么都不匹配

                          /)                          # 普通闭括弧

                      " , RegexOptions .IgnorePatternWhitespace);

对于一个嵌套结构而言,开始和结束标记都是确定的,对于本例开始为“ ( ”,结束为“ ) ”,那么接下来就是考察中间的结构,中间的字符可以划分为三类,一类是“ ( ”,一类是“ ) ”,其余的就是除这两个字符以外的任意字符。

那么平衡组的匹配原理就是这样的:

1.          先找到第一个“ ( ”,作为匹配的开始

2.          在第 1 步以后,每匹配到一个“ ( ”,就入栈一个 Open 捕获组,计数加 1

3.          在第 1 步以后,每匹配到一个“ ) ”,就出栈最近入栈的 Open 捕获组,计数减 1

4.          后面的 (?(Open)(?!)) 用来保证堆栈中 Open 捕获组计数是否为 0 ,也就是“ ( ”和“ ) ”是配对出现的

5.          最后的“ ) ”,作为匹配的结束

匹配过程(以下匹配过程,如果觉得难以理解,可以暂时跳过,先学会如何使用,再研究为什么可以这样用吧)

首先匹配第一个“ ( ”,然后一直匹配,直到出现以下两种情况之一:

a)           堆栈中 Open 计数已为 0 ,此时再遇到“ )

b)           匹配到字符串结束符

这时控制权交给 (?(Open)(?!)) ,判断 Open 是否有匹配,由于此时计数为 0 ,没有匹配,那么就匹配“ no ”分支,由于这个条件判断结构中没有“ no ”分支,所以什么都不做,把控制权交给接下来的“ /)

如果上面遇到的是情况 a) ,那么此时“ /) ”可以匹配接下来的“ /) ”,匹配成功;如果上面遇到的是情况 b) ,那么此时会进行回溯,直到“ /) ”匹配成功为止,否则报告整个表达式匹配失败。

由于 .NET 中的狭义平衡组“ (?<Close-Open>Expression) ”结构,可以动态的对堆栈中捕获组进行计数,匹配到一个开始标记,入栈,计数加 1 ,匹配到一个结束标记,出栈,计数减 1 ,最后再判断堆栈中是否还有 Open ,有则说明开始和结束标记不配对出现,不匹配,进行回溯或报告匹配失败;如果没有,则说明开始和结束标记配对出现,继续进行后面子表达式的匹配。

需要对“ (?!) ”进行一下说明,它属于顺序否定环视,完整的语法是“ (?!Expression) ”。由于这里的“ Expression ”不存在,表示这里不是一个位置,所以试图尝试匹配总是失败的,作用就是在 Open 不配对出现时,报告匹配失败。

3        平衡组的应用及优化

平衡组提供了嵌套结构的匹配功能,这一创新是很让人兴奋的,因为此前正则对于嵌套结构的匹配是无能为力的。然而功能的强大,自然也带来了实现的复杂,正则书写得不好,可能会存在效率陷阱,甚至导致程序崩溃,这里介绍一些基本的优化方法。

3.1      单字符嵌套结构平衡组优化

单字符的嵌套结构指的是开始和结束标记都单个字符的嵌套结构,这种嵌套相对来说比较简单,优化起来也比较容易。先从上面提到的例子开始。

3.1.1   贪婪与非贪婪模式

上面给的例子是一种做了部分优化的常规写法,算作是版本 1 吧,它做了哪些优化呢,先来看下完全没有做过优化的版本 0 吧。

string test = "a+(b*(c+d))/e+f-(g/(h-i))*j" ;

Regex reg0 = new Regex (@"/(                          # 普通字符“(”

                               (                       # 分组构造,用来限定量词“*” 修饰范围

                                  (?<Open>/()         # 命名捕获组,遇到开括弧Open 计数加1

                             |                       # 分支结构

                                 (?<-Open>/))        # 狭义平衡组,遇到闭括弧Open 计数减1

                             |                       # 分支结构

                                 .                   # 任意字符

                             )*?                     # 以上子串出现0 次或任意多次,非贪婪模式

                             (?(Open)(?!))           # 判断是否还有'OPEN' ,有则说明不配对,什么都不匹配

                         /)                          # 普通闭括弧

                        " , RegexOptions .IgnorePatternWhitespace);

MatchCollection mc = reg0.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "/n" ;

}

/*-------- 输出--------

(b*(c+d))

(g/(h-i))

*/

接下来对比一下版本1

Regex reg1 = new Regex (@"/(                          # 普通字符“(”

                            (                       # 分组构造,用来限定量词“*” 修饰范围

                                (?<Open>/()         # 命名捕获组,遇到开括弧’Open’ 计数加1

                            |                       # 分支结构

                                  (?<-Open>/))        # 狭义平衡组,遇到闭括弧’Open’ 计数减1

                            |                       # 分支结构

                                  [^()]+              # 非括弧的其它任意字符

                            )*                      # 以上子串出现0 次或任意多次

                            (?(Open)(?!))           # 判断是否还有’Open’ ,有则说明不配对,什么都不匹配

                        /)                          # 普通闭括弧

                      " , RegexOptions .IgnorePatternWhitespace);

看到区别了吗?版本 1 对版本 0 的改进主要有两个地方,一个是用“ [^()]+ ”来代替“ . ”,另一个是用“ * ”来代替“ *? ”,也就是用贪婪模式来代替非贪婪模式。

如果使用了小数点“ . ”,那么为什么不能在分组内使用“ .+ ”,后面又为什么不能用“ * ”呢?只要在上面的正则中使用并运行一下代码就可以知道了,匹配的结果是

(b*(c+d))/e+f-(g/(h-i))

而不是

(b*(c+d))

(g/(h-i))

因为无论是分组内使用“ .+ ”还是后面使用“ * ”,都是贪婪模式,所以小数点会一直匹配下去,直到匹配到字符串的结束符才会停止,然后进行回溯匹配。为了取得正确结果,必须使用非贪婪模式“ *? ”。

这就类似于用“ /(.+/) ”去匹配“ (abc)def(ghi) ”一样,得到的结果是“ (abc)def(ghi) ”,而不是通常我们希望的“ (abc) ”和“ (ghi) ”。这时要用非贪婪模式“ /(.+?/) ”来得到正确的结果。

贪婪模式和非贪婪模式在匹配失败时,回溯的次数基本上是一样的,效率上没有多大区别,但是在匹配成功时,贪婪模式比非贪婪模式回溯的次数要少得多,效率要高得多。

对于“ /(.+/) ”如果既要得到正确的匹配结果,又要提高匹配效率,可以使用排除型捕获组 + 贪婪模式的方式,即“ /([^()]+/) ”。

版本 0 的平衡组也是一样,可以使用排除字符组“ [^()]+ ”和贪婪模式“ * ”结合的方式,提高匹配效率,得到的就是版本 1 的平衡组。

相对于版本 0 ,或许你会认为版本 1 的写法是很自然的,但是如果不了解这样一个演进过程,那么在字符序列嵌套结构平衡组优化时,就不会是那么自然的一件事了。

3.1.2   分支结构

接下来就是分支结构的优化。

语法: (Exp1|Exp2|Exp3)

因为分支结构的匹配规则是,从左向右尝试匹配,当左侧分支匹配成功时,就不再向右尝试。所以使用分支结构时,可以根据以下两条规则进行优化:

1.          尽量抽象出每个分支中的公共的部分,使最后的表达式中,每个分支共公部分尽可能的少,比如 (this|that) 的匹配效率是没有 th(is|at) 高的。

2.          在不影响匹配结果的情况下,把出现概率高的分支放在左侧,出现概率低的分支放右侧。

对于本例中的分支结构,已经没有公共部分,符合第一条规则,再看下第二条规则,开始标记“ ( ”和结束标记“ ) ”出现的概率基本上是一样的,而除“ ( ”和“ ) ”之外的字符出现的概率是比“ ( ”和“ ) ”出现的概率高的,所以应该把“ [^()]+ ”分支放在左侧。

版本 1 由于采用了排除型捕获组,所以这三个分支没有包含关系,左右顺序对结果不会造成影响,可以调整顺序。因为这是已经经过优化的了,而如果是版本 0 ,由“ . ”对“ ( ”和“ ) ”有包含关系,就不能调整顺序了。

在版本 1 基础上对分支结构进行优化后,就得到版本 2

string test = "a+(b*(c+d))/e+f-(g/(h-i))*j" ;

Regex reg2 = new Regex (@"/(                          # 普通字符“(”

                             (                       # 分组构造,用来限定量词“*” 修饰范围

                                 [^()]+              # 非括弧的其它任意字符

                             |                       # 分支结构

                                 (?<Open>/()         # 命名捕获组,遇到开括弧Open 计数加1

                              |                       # 分支结构

                                  (?<-Open>/))        # 狭义平衡组,遇到闭括弧Open 计数减1

                              )*                      # 以上子串出现0 次或任意多次

                             (?(Open)(?!))           # 判断是否还有'OPEN' ,有则说明不配对,什么都不匹配

                         /)                          # 普通闭括弧

                         " , RegexOptions .IgnorePatternWhitespace);

MatchCollection mc = reg2.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "/n" ;

}

/*-------- 输出--------

(b*(c+d))

(g/(h-i))

*/

3.1.3   捕获组

这里面主要涉及到了两个捕获组“ (?<Open>/() ”和“ (?<-Open>/)) ”,而在平衡组的应用中,我是只关心它是否匹配了,而对于匹配到的内容是不关心的。对于这样一种需求,可以用以下方式实现

/( (?<Open>)

/)(?<-Open>)

(?<Open>) ”和“ (?<-Open>) ”这两种方式只是使用了命名捕获组,捕获的是一个位置,它总是能够匹配成功的,而匹配的内容是空的,分配的内存空间是固定的,可以有效的节省资源,这在单字符嵌套结构中并不明显,但是在字符序列嵌套结构中就比较明显了。

由于捕获组是直接跟在开始或结束标记之后的,所以只要开始或结束标记匹配成功,命名捕获组自然就会匹配成功,对于功能是没有任何影响的。

那 么把标记和捕获组调整一下顺序是否可以呢?从功能上来讲,是可以的,但是匹配的流程上会有所不同,先是捕获组匹配成功,入栈,然后再匹配标记,成功则继续 匹配,不成功则该分支匹配失败,进行回溯,出栈,继续尝试下一分支。这样将增加许多入栈和出栈的操作,对匹配效率是有影响的,所以这种方式并不可取。

在版本 2 基础上对捕获组进行优化后,就得到版本 3

string test = "a+(b*(c+d))/e+f-(g/(h-i))*j" ;

Regex reg3 = new Regex (@"/(                          # 普通字符“(”

                             (                       # 分组构造,用来限定量词“*” 修饰范围

                                 [^()]+              # 非括弧的其它任意字符

                             |                       # 分支结构

                                 /(  (?<Open>)        # 命名捕获组,遇到开括弧Open 计数加1

                              |                       # 分支结构

                                  /)  (?<-Open>)      # 狭义平衡组,遇到闭括弧Open 计数减1

                              )*                      # 以上子串出现0 次或任意多次

                             (?(Open)(?!))           # 判断是否还有'OPEN' ,有则说明不配对,什么都不匹配

                         /)                          # 普通闭括弧

                         " , RegexOptions .IgnorePatternWhitespace);

MatchCollection mc = reg3.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "/n" ;

}

/*-------- 输出--------

(b*(c+d))

(g/(h-i))

*/

3.1.4   固化分组

看到有些人使用平衡组时用到了固化分组,但并不是所有人都明白固化分组的作用。

语法: (?>Expression)

用“ /([^()]+/) ”去匹配“ (abc) ”是可以匹配成功的,因为不用回溯,相对于“ /(.+?/) ”这种非贪婪模式,效率上有所提升,但是对于匹配失败的情况又如何呢?

源字符串: (abc

正则表达式: /([^()]+/)

匹配中间过程这里不再详述,可以参考 NFA 引擎匹配原理

当“ [^()]+ ”匹配到结束位置时,控制权交给“ /) ”,匹配失败,进行回溯,而由于前面使用了“ [^()]+ ”这种排除型字符组,所以可供回溯的位置,不会存在可以匹配“ /) ”的情况,这时候的回溯是完全没有意义的,只会浪费时间,但是由于传统 NFA 引擎的特点,必须回溯所有可能之后才会报告匹配失败。

这时可以用固化分组来进行优化,一旦占有字符,就不再释放。也就是一旦占有,就不再记录可供回溯的可能。通常是与排除型字符组或顺序否定环视一起使用的。

优化后的正则表达式: /((?>[^()]+)/)

需要说明的一点,固化分组要作用于量词修饰的子表达式才有意义,对于“ (?>abc) ”由于内容是固定的,根本就不会产生回溯,所以使用固化分组是没有意义的。

对于平衡组的应用也是一样,如果分组构造中没有量词,那么使用固化分组就是没有意义的,比如版本 0

Regex reg = new Regex (@"/((?>(?<Open>/()|(?<-Open>/))|.)*?(?(Open)(?!))/)" );

这种场景下使用固化分组就是没有意义的。

在版本 3 基础上对捕获组进行优化后,就得到版本 4

string test = "a+(b*(c+d))/e+f-(g/(h-i))*j" ;

Regex reg4 = new Regex (@"/(                           # 普通字符“(”

                             (?>                     # 分组构造,用来限定量词“*” 修饰范围

                                 [^()]+              # 非括弧的其它任意字符

                             |                       # 分支结构

                                 /(  (?<Open>)        # 命名捕获组,遇到开括弧Open 计数加1

                              |                       # 分支结构

                                  /)  (?<-Open>)      # 狭义平衡组,遇到闭括弧Open 计数减1

                              )*                      # 以上子串出现0 次或任意多次

                             (?(Open)(?!))           # 判断是否还有'OPEN' ,有则说明不配对,什么都不匹配

                         /)                          # 普通闭括弧

                         " , RegexOptions .IgnorePatternWhitespace);

MatchCollection mc = reg4.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "/n" ;

}

/*-------- 输出--------

(b*(c+d))

(g/(h-i))

*/

那么对于分组构造外层的“ * ”修饰的子表达式是否可以使用固化分组呢?答案是否定的,因为平衡组通常是要进行回溯才能最终匹配成功的,所以如果使用固化分组,不记录回溯可能的话,将无法得到正确结果。

3.1.5   进一步优化讨论

那么现在是不是已经完成优化了呢?是的,通常可以这么认为。在一般应用当中,这已经是从正则层面上来说,最优方案了。

但是在有些场景下,由于 Compiled 模式可以有效提高分支结构的匹配效率,所以对于源字符串比较复杂的情况,牺牲一些编译时间和内存,还是可以有效提高匹配效率的。

Regex reg5 = new Regex (@"/(                         # 普通字符“(”

                             (?>                      # 分组构造,用来限定量词“*” 修饰范围

                                [^()]+              # 非括弧的其它任意字符

                             |                       # 分支结构

                                 /(  (?<Open>)        # 命名捕获组,遇到开括弧Open 计数加1

                              |                       # 分支结构

                                  /)  (?<-Open>)      # 狭义平衡组,遇到闭括弧Open 计数减1

                              )*                      # 以上子串出现0 次或任意多次

                             (?(Open)(?!))           # 判断是否还有'OPEN' ,有则说明不配对,什么都不匹配

                         /)                          # 普通闭括弧

                         " , RegexOptions .IgnorePatternWhitespace | RegexOptions. Compiled ) ;

MatchCollection mc = reg5.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "/n" ;

}

/*-------- 输出--------

(b*(c+d))

(g/(h-i))

*/

并不是所有应用场景都适合使用 Compiled 模式,比如上面这个例子里的源字符串如果是“ a+(b*(c+d))/e+f-(g/(h-i))*j ”,本身是非常简单的,使用 Compiled 模式将是得不偿失的。什么时候使用,要根据具体问题具体分析。

3.2      字符序列嵌套结构平衡组应用

字符序列嵌套结构的匹配,典型的应用就是 html 标签的提取。由于上面详细说明了单字符嵌套结构的优化过程,这里主要讲应用场景,个别涉及到优化的地方再讨论。

字符序列嵌套结构的匹配,举例来说,取 div 标签。源字符串如下:

< div id ="0">

    0

</ div >

< div id ="1">

    1

    < div id ="2">

        2

</ div >

</ div >

3.2.1   提取最外层嵌套结构

提取最外层 div 标签,分析过程及构造方式与单字符嵌套结构差不多,只是捕获组等内容稍稍复杂点,先给出实现,再进行解释。

string test = @"<div id=""0"">

    0

</div>

<div id=""1"">

    1

    <div id=""2"">

        2

    </div>

</div>" ;

Regex reg = new Regex (@"(?isx)                      # 匹配模式,忽略大小写,“. ”匹配任意字符

                       <div[^>]*>                      # 开始标记“<div...>”

                           (?>                         # 分组构造,用来限定量词“*” 修饰范围

                               <div[^>]*>  (?<Open>)   # 命名捕获组,遇到开始标记,入栈,Open 计数加1

                              |                           # 分支结构

                               </div>  (?<-Open>)      # 狭义平衡组,遇到结束标记,出栈,Open 计数减1

                           |                           # 分支结构

                               (?:(?!</?div/b).)*      # 右侧不为开始或结束标记的任意字符

                           )*                          # 以上子串出现0 次或任意多次

                           (?(Open)(?!))               # 判断是否还有'OPEN' ,有则说明不配对,什么都不匹配

                       </div>                          # 结束标记“</div>”

                       " );

MatchCollection mc = reg.Matches(test);

foreach (Match m in mc)

{

richTextBox2.Text += m.Value + "/n--------------------/n" ;

}

/*-------- 输出--------

<div id="0">

    0

</div>

--------------------

<div id="1">

    1

    <div id="2">

        2

    </div>

</div>

--------------------

*/

在单字符嵌套结构中,使用排除型字符组“ [^()]+ ”,与分组构造外的匹配优先量词“ * 达到贪婪模式匹配效果。在字符序列嵌套结构中,要排除的是一个子串,而不是简单的几个无序字符,所以不能使用排除型字符组,此时需要用到顺序否定环视来达到这一目的。“ (?:(?!</?div/b).)* ”表示的是所在位置右侧不是“ <div…> ”或“ </div> ”的字符,这样的字符重复 0 次或任意多次。关于环视的细节,可以参考 正则基础之——环视

而由于这种否定环视包含两种状态,所以在与固化分组结合使用时,会与后面的开始或结束标记形成包含关系,所以与固化分组一起使用时,不能放在左侧,只能放在右侧。

3.2.2   根据 id 提取 div 嵌套标签

根据 id 提取 div 时,改变的只是最外层 div 的结构,对内分组构造内部结构没有影响。但是因为 id 是变化的,所以正则需要动态生成。下面给出实现,源字符串和输出结果由于比较影响篇幅,就不再给出了。

string id = Regex .Escape(textBox1.Text);                    // 动态获取id

Regex reg = new Regex (@"(?isx)

                      <div(?:(?!(?:id=|</?div/b)).)*id=(['""]?)" + id  + @"/1[^>]*>        # 开始标记“<div...>”

                          (?>                          # 分组构造,用来限定量词“*” 修饰范围

                                <div[^>]*>  (?<Open>)   # 命名捕获组,遇到开始标记,入栈,Open 计数加1

                            |                           # 分支结构

                                 </div>  (?<-Open>)      # 狭义平衡组,遇到结束标记,出栈,Open 计数减1

                            |                           # 分支结构

                                (?:(?!</?div/b).)*      # 右侧不为开始或结束标记的任意字符

                            )*                          # 以上子串出现0 次或任意多次

                            (?(Open)(?!))               # 判断是否还有'OPEN' ,有则说明不配对,什么都不匹配

                       </div>                          # 结束标记“</div>”

                     " );

MatchCollection mc = reg.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "/n--------------------/n" ;

}

在动态生成正则表达式时,由于输入的字符串中可能存在正则中有特殊意义的元字符,如果不进行转义的话,正则解析时会抛出异常。所以用 Regex .Escape(string str) 来对动态输入的字符串进行转义处理,确保不会因动态输入的内容而抛异常。比如上面的例子,如果 id 不进行转义处理时,输入“ abc(def ”就会抛“ ) 不足”这样的异常。

3.2.3   根据 id 提取任意嵌套标签

再扩展一下,根据 id 属性取任意嵌套标签。实现如下,具体实现细节和讨论参考 就是通过 id 获得一个 html 标签块 。以下正则相对于帖子对个别细节做了调整。

string html = @"

<html>

<body>

<div id=""div1"">

    <div id=""div2"" style=""background:Red;"">

        <div id=""div3"">

            <table id=""table1"">

                <tr>

                    <td>

                        <div id=""div4"" style=""width:100px""></div>

                    </td>

                </tr>

            </table>

        </div>

    </div>

    <div id=div5>

        <a href=""http://www.csdn.net"">csdn</a>

    </div>

</div>

<img src=""http://www.csdn.net/Images/logo_csdn.gif""/>

</body>

</html>" ;

Console .WriteLine(html);

string [] idList = { "div1" , "div2" , "div3" , "div4" , "table1" , "div5" , "abc(def" };

string pattern = @"<([a-z]+)(?:(?!/bid/b)[^<>])*id=([""']?){0}/2[^>]*>(?></1[^>]*>(?<o>)|<//1>(?<-o>)|(?:(?!</?/1).)*)*(?(o)(?!))<//1>" ;

foreach (string id in idList)

{

     Match match = Regex .Match(html, string .Format(pattern, Regex .Escape(id)),

                    RegexOptions .Singleline | RegexOptions .IgnoreCase);

      Console .WriteLine("--------begin {0}--------" , id);

     if (match.Success)

          Console .WriteLine(match.Value);

     else

           Console .WriteLine("o( )o" );

      Console .WriteLine("--------end {0}--------" , id);

}

Console .ReadLine();

3.2.4   根据标签取外层嵌套结构

根据动态输入的 tag ,取相应的最外层的嵌套标签,实现如下。

string html = @"

<html>

<body>

<div id=""div1"">

    <div id=""div2"" style=""background:Red;"">

        <div id=""div3"">

            <table id=""table1"">

                <tr>

                    <td>

                        <div id=""div4"" style=""width:100px""></div>

                    </td>

                </tr>

            </table>

        </div>

    </div>

    <div id=div5>

        <a href=""http://www.csdn.net"">csdn</a>

    </div>

</div>

<img src=""http://www.csdn.net/Images/logo_csdn.gif""/>

</body>

</html>" ;

Console .WriteLine(html);

string [] tagList = { "html" , "body" , "div" , "table" , "abc(def" };

string pattern = @"(?isx)

                      <({0})/b[^>]*>                   # 开始标记“<tag...>”

                          (?>                         # 分组构造,用来限定量词“*” 修饰范围

                              </1[^>]*>  (?<Open>)    # 命名捕获组,遇到开始标记,入栈,Open 计数加1

                          |                           # 分支结构

                              <//1>  (?<-Open>)       # 狭义平衡组,遇到结束标记,出栈,Open 计数减1

                          |                           # 分支结构

                               (?:(?!</?/1/b).)*       # 右侧不为开始或结束标记的任意字符

                          )*                          # 以上子串出现0 次或任意多次

                          (?(Open)(?!))               # 判断是否还有'OPEN' ,有则说明不配对,什么都不匹配

                      <//1>                           # 结束标记“</tag>”

                     " ;

foreach (string tag in tagList)

{

     Match match = Regex .Match(html, string .Format(pattern, Regex .Escape(tag)));

     Console .WriteLine("--------begin {0}--------" , tag);

     if (match.Success)

         Console .WriteLine(match.Value);

     else

         Console .WriteLine("o( )o" );

    Console .WriteLine("--------end {0}--------" , tag);

}

Console .ReadLine();

3.2.5   条件判断结构扩展应用

条件判断结构的作用不只限于验证开始和结束标记是否配对,根据需求的不同,还可以有其它一些应用。比如在匹配 div 标签时,只取内部“存在”嵌套的外层标签。

string test = @"<div id=""0"">

    0

</div>

<div id=""1"">

    1

    <div id=""2"">

        2

    </div>

</div>" ;

Regex reg = new Regex (@"(?isx)                                # 匹配模式,忽略大小写,“.” 匹配任意字符

                      <div[^>]*>                              # 开始标记“<div...>”

                          (?>                                 # 分组构造,用来限定量词“*” 修饰范围

                              <div[^>]*>  (?<Open>)(?<Mask>)  # 遇到开始标记,入栈,OpenMask 计数各加1

                          |                                    # 分支结构

                              </div>  (?<-Open>)              # 遇到结束标记,出栈,Open 计数减1

                          |                                   # 分支结构

                              (?:(?!</?div/b).)*              # 右侧不为开始或结束标记的任意字符

                          )*                                  # 以上子串出现0 次或任意多次

                          (?(Open)(?!))(?(Mask)|(?!))         #'OPEN' 保证标记配对,'Mask' 保证内部有嵌套

                      </div>                                  # 结束标记“</div>”

                       " );

MatchCollection mc = reg.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "/n--------------------/n" ;

}

/*-------- 输出--------

<div id="1">

    1

    <div id="2">

        2

    </div>

</div>

--------------------

*/

命名捕获组“ (?<Mask>) ”只入栈不出栈,如果内部有嵌套,则“ (?<Mask>) ”一定有匹配,此时匹配“ (?(Mask)yes|no) ”中的“ yes ”子表达式,也就是什么都不做;如果内部没有嵌套,则“ (?<Mask>) ”没有匹配,此时匹配“ (?(Mask)yes|no) ”中的“ no ”子表达式,也就是报告匹配失败。这里省略的是“ (?(Mask)yes|no) ”中的“ yes ”子表达式。

对于匹配内部没有嵌套的标签,也就是最内层标签,可以使用上面的正则表达式,将“ (?(Mask)yes|no) ”中的“ yes ”子表达式设为“ (?!) ”,将“ yes ”子表达式省略。不过这样做有些浪费,完全可以用顺序否定环视来实现这一需求。

string test = @"<div id=""0"">

    0

</div>

<div id=""1"">

    1

    <div id=""2"">

        2

    </div>

</div>" ;

Regex reg = new Regex (@"(?is)<div[^>]*>(?:(?!</?div/b).)*</div>" );

MatchCollection mc = reg.Matches(test);

foreach (Match m in mc)

{

      richTextBox2.Text += m.Value + "/n--------------------/n" ;

}

/*-------- 输出--------

<div id="0">

    0

</div>

--------------------

<div id="2">

        2

    </div>

--------------------

*/

4        平衡组应用范围探讨

平衡组可以用来匹配嵌套结构,这是一个很大的创新,但是否就认为平衡组适合用来解决任何嵌套问题呢?事实当然不会是这样。

比如下面这个需求, ( 参考 请问一个正则表达式 )

源字符串: 1+Sum(1,Sum(2, Sum(3), 4), 5)*4+5+Sum(9,Sum(8, Sum(7), 6), 5)*6+7

要求输出:

Sum(1,Sum(2, Sum(3), 4), 5)

Sum(2, Sum(3), 4)

Sum(3)

Sum(9,Sum(8, Sum(7), 6), 5)

Sum(8, Sum(7), 6)

Sum(7)

这种需求使用平衡组 + 递归的方式可以实现,实现代码如下:

// 递归方法

private void getNesting(string src, Regex reg, List <string > list)

{

    MatchCollection mc = reg.Matches(src);

    foreach (Match m in mc)

    {

        list.Add(m.Value);

        src = m.Value.Remove(m.Value.Length-1, 1);

        if (reg.IsMatch(src))

        {

             getNesting(src, reg, list);

        }

    }

}

// 调用

string test = "1+Sum(1,Sum(2, Sum(3), 4), 5)*4+5+Sum(9,Sum(8, Sum(7), 6), 5)*6+7" ;

List <string > list = new List <string >();

Regex reg = new Regex (@"(?i)Sum/((?>[^()]+|/((?<o>)|/)(?<-o>))*(?(o)(?!))/)" , RegexOptions .Compiled);

getNesting(test, reg, list);

foreach (string s in list)

{

     richTextBox2.Text += s + "/n" ;

}

平衡组虽然可以实现要求,但除非你对效率没有要求,否则这一类需求通常是不适合用正则来实现的。因为平衡组并不是为这一功能而设计的,在实现过程中做了很多额外的尝试。效率上自然要大打折扣。

类似这样的需求,可以自己写有穷自动机来实现,毕竟正则也只不过是一种有穷自动机的实现而已。

            string test = @"1+Sum(1,Sum(2, Sum(3), 4), 5)*4+5+Sum(9,Sum(8, Sum(7), 6), 5)*6+7 " ;

 

            StringBuilder nesting = new StringBuilder (64);

            List <StringBuilder > list = new List <StringBuilder >();

            List <string > groups = new List <string >();

 

            int level = 0;

            int state = 0;

 

            foreach (char c in test)

            {

                if ((c == 'S' || c == 's' ) && state == 0)

                {

                    state = 1;

                    nesting.Append(c);

                }

                else if ((c == 'U' || c == 'u' ) && state == 1)

                {

                    state = 2;

                    nesting.Append(c);

                }

                else if ((c == 'M' || c == 'm' ) && state == 2)

                {

                    state = 3;

                    nesting.Append(c);

                }

                else if (c == '(' && state == 3)

                {

                    state = 0;

                    level++;

                }

                else

                {

                     state = 0;

                    nesting = new StringBuilder (64);

                }

 

                if (c == ')' )

                {

                    if (level > 0)

                    {

                        level--;

                        groups.Add(list[level].ToString() + c);

                        list.Remove(list[level]);

                    }

                }

 

                if (level > 0)

                {

                    while (list.Count < level)

                     {

                        list.Add(nesting);

                    }

                    for (int i = 0; i < level; i++)

                    {

                        list[i].Append(c);

                    }

                }

            }

 

            foreach (string s in groups)

            {

                Console .WriteLine(s);

            }

            Console .ReadLine();

5        其它声明

到此为止,平衡组的基本应用场景和性能调优都已讨论完了,本文对于平衡组匹配原理讲得相对比较少,以应用场景分析为主。主要是因为能够使用平衡组来解决问题的人,通常已经对正则的基本语法有了一定程度的理解。而如果事实确实如此,那么对于平衡组的理解,也是水到渠成的了。

以上正则实现中,采用的多是宽松排列模式,主要是为了加注释,使得阅读清晰。而宽松排列模式通常用于教学目的,实际使用过程中,如果不是为了可读性的考虑,可以去掉这些注释和宽松排列模式参数。

上 面给出了很多平衡组的应用,这里需要说明的是,我提供的只是一些方法和思路,从来不推荐把正则当作模板来用,虽然有些时候,它确实可以当作模板来用,但我 还是希望你能真正的掌握这些语法规则之后,再去应用平衡组。当然,如果你认为能用就行,不需要知道为什么可以这样用,只是把它当作模板来套,我也无话可 说。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值