猫猫杰 發表於 2023-5-28 13:50:54

.NET正则基础之平衡组

<h2>1、概述</h2>
<p>平衡组是微软在.NET中提出的一个概念,主要是结合几种正则语法规则,提供对配对出现的嵌套结构的匹配。.NET是目前对正则支持最完备、功能最强大的语言平台之一,而平衡组正是其强大功能的外在表现,也是比较实用的文本处理功能,目前只有.NET支持,相信后续其它语言会提供支持。</p>
<p>平衡组可以有狭义和广义两种定义,狭义平衡组指.NET中定义的(?&lt;Close-Open&gt;Expression)语法,广义平衡组并不是固定的语法规则,而是几种语法规则的综合运用,我们平时所说的平衡组通常指的是广义平衡组。本文中如无特殊说明,平衡组这种简写指的是广义平衡组。</p>
<p>正是由于平衡组功能的强大,所以带来了一些神秘色彩,其实平衡组并不难掌握。下面就平衡组的匹配原理、应用场景以及性能调优展开讨论。</p>
<h2>2、平衡组匹配原理</h2>
<h3>2.1 预备知识</h3>
<p>平衡组通常是由量词,分支结构,命名捕获组,狭义平衡组,条件判断结构组成的,量词和分支结构这里不做介绍,这里只对命名捕获组,狭义平衡组和条件判断结构做下说明。</p>
<h3>2.1.1命名捕获组</h3>
<p>语法:(?&lt;name&gt;Expression) &nbsp;</p>
<p>&nbsp;(?&rsquo;name&rsquo;Expression)</p>
<p>以上两种写法在.NET中是等价的,都是将&ldquo;Expression&rdquo;子表达式匹配到的内容,保存到以&ldquo;name&rdquo;命名的组里,以供后续引用。</p>
<p>对于命名捕获组的应用,这里不做重点介绍,只是需要澄清一点,平时使用捕获组时,一般反向引用或Group对象使用得比较多,可能会有一种误解,那就是捕获组只保留一个匹配结果,即使一个捕获组可以先后匹配多个子串,也只保留最后一个匹配到的子串。但事实是这样吗?</p>
<p>举例来说:</p>
<p>源字符串:abcdefghijkl</p>
<p>正则表达式:(?&lt;chars&gt;{2})+</p>
<p>命名捕获组chars最终捕获的是什么?</p>
<div class="jb51code"><pre class="brush:csharp;">string test = "abcdefghijkl";
Regex reg = new Regex(@"(?&lt;chars&gt;{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
*/</pre></div>
<p>从m.Groups[&quot;chars&quot;].Value的输出上看,似乎确实是只保留了一个匹配内容,但却忽略了一个事实,Group实际上是Capture的一个集合</p>
<div class="jb51code"><pre class="brush:csharp;">string test = "abcdefghijkl";
Regex reg = new Regex(@"(?&lt;chars&gt;{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
*/</pre></div>
<p>平时应用时可能会忽略这一点,因为很少遇到一个捕获组先后匹配多个子串的情况,而在一个捕获组只匹配一个子串时,Group集合中就只有一个Capture元素,所以内容是一样的。</p>
<div class="jb51code"><pre class="brush:csharp;">string test = "abcdefghijkl";
Regex reg = new Regex(@"(?&lt;chars&gt;{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
*/</pre></div>
<p>捕获组保存的是一个集合,而不只是一个元素,这一知识点对于理解平衡组的匹配原理是有帮助的。</p>
<h4>2.1.2狭义平衡组</h4>
<p>语法:(?&lt;Close-Open&gt;Expression)</p>
<p>其中&ldquo;Close&rdquo;是命名捕获组的组名,也就是&ldquo;(?&lt;name&gt;Expression)&rdquo;中的&ldquo;name&rdquo;,可以省略,通常应用时并不关注,所以一般都是省略的,写作&ldquo;(?&lt;-Open&gt;Expression)&rdquo;。作用就是当此处的&ldquo;Expression&rdquo;子表达式匹配成功时,则将最近匹配成功到的命名为&ldquo;Open&rdquo;组出栈,如果此前不存在匹配成功的&ldquo;Open&rdquo;组,那么就报告&ldquo;(?&lt;-Open&gt;Expression)&rdquo;匹配失败,整个表达式在这一位置也是匹配失败的。</p>
<h4>2.1.3条件判断结构</h4>
<p>语法:(?(Expression)yes|no)</p>
<p>&nbsp; &nbsp; &nbsp; (?(name)yes|no)</p>
<p>对于&ldquo;(?(Expression)yes|no)&rdquo;,它是&ldquo;(?(?=Expression)yes|no)&rdquo;的简写形式,相当于三元运算符</p>
<p>(?=Expression) ? yes : no</p>
<p>表示如果子表达式&ldquo;(?=Expression)&rdquo;匹配成功,则匹配&ldquo;yes&rdquo;子表达式,否则匹配&ldquo;no&rdquo;子表达式。如果&ldquo;Expression&rdquo;与可能出现的命名捕获组的组名相同,为避免混淆,可以采用&ldquo;(?(?=Expression)yes|no)&rdquo;方式显示声明&ldquo;Expression&rdquo;为子表达式,而不是捕获组名。</p>
<p>&ldquo;(?=Expression)&rdquo;验证当前位置右侧是否能够匹配&ldquo;Expression&rdquo;,属于顺序环视结构,是零宽度的,所以它只参与判断,即使匹配成功,也不会占有字符。</p>
<p>举例来说:</p>
<p>源字符串:abc</p>
<p>正则表达式:(?(?=a)\w{2}|\w)</p>
<p>当前位置右侧如果是字符&ldquo;a&rdquo; ,则匹配两个&ldquo;\w&rdquo;,否则匹配一个&ldquo;\w&rdquo;。</p>
<div class="jb51code"><pre class="brush:csharp;">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
*/</pre></div>
<p>对于&ldquo;(?(name)yes|no)&rdquo;,如果命名捕获组&ldquo;name&rdquo;有捕获,则匹配&ldquo;yes&rdquo;子表达式,否则匹配&ldquo;no&rdquo;子表达式。这一语法最典型的一种应用是平衡组。</p>
<p>当然,以上两种语法中,&ldquo;yes&rdquo;和&ldquo;no都是可以省略的,但同一时间只能省略一个,不能一起省略。平衡组的应用中就是省略了&ldquo;no&rdquo;子表达式。</p>
<h3>2.2平衡组的匹配原理</h3>
<p>平衡组的匹配原理可以用堆栈来解释,先举个例子,再根据例子进行解释。</p>
<p>源字符串:a+(b*(c+d))/e+f-(g/(h-i))*j</p>
<p>正则表达式:)|[^()])*(?(Open)(?!))\)</p>
<p>需求说明:匹配成对出现的()中的内容</p>
<div class="jb51code"><pre class="brush:csharp;">string test = "a+(b*(c+d))/e+f-(g/(h-i))*j";
Regex reg = new Regex(@")|[^()])*(?(Open)(?!))\)");
MatchCollection mc = reg.Matches(test);
foreach (Match m in mc)
{
     richTextBox2.Text += m.Value + "\n";
}
/*--------输出--------
(b*(c+d))
(g/(h-i))
*/</pre></div>
<p>下面来考察一下这个正则,为了阅读方便,写成宽松模式。</p>
<div class="jb51code"><pre class="brush:csharp;">Regex reg = new Regex(@"\(                          #普通字符“(”
                            (                       #分组构造,用来限定量词“*”修饰范围
                                (?&lt;Open&gt;\()         #命名捕获组,遇到开括弧'Open'计数加1
                            |                       #分支结构
                                (?&lt;-Open&gt;\))        #狭义平衡组,遇到闭括弧'Open'计数减1
                            |                       #分支结构
                                [^()]+              #非括弧的其它任意字符
                            )*                      #以上子串出现0次或任意多次
                            (?(Open)(?!))           #判断是否还有'Open',有则说明不配对,什么都不匹配
                        \)                          #普通闭括弧
                     ", RegexOptions.IgnorePatternWhitespace);</pre></div>
<p>对于一个嵌套结构而言,开始和结束标记都是确定的,对于本例开始为&ldquo;(&rdquo;,结束为&ldquo;)&rdquo;,那么接下来就是考察中间的结构,中间的字符可以划分为三类,一类是&ldquo;(&rdquo;,一类是&ldquo;)&rdquo;,其余的就是除这两个字符以外的任意字符。</p>
<p>那么平衡组的匹配原理就是这样的:</p>
<p>1.先找到第一个&ldquo;(&rdquo;,作为匹配的开始</p>
<p>2.在第1步以后,每匹配到一个&ldquo;(&rdquo;,就入栈一个Open捕获组,计数加1</p>
<p>3.在第1步以后,每匹配到一个&ldquo;)&rdquo;,就出栈最近入栈的Open捕获组,计数减1</p>
<p>4.后面的(?(Open)(?!))用来保证堆栈中Open捕获组计数是否为0,也就是&ldquo;(&rdquo;和&ldquo;)&rdquo;是配对出现的</p>
<p>5.最后的&ldquo;)&rdquo;,作为匹配的结束</p>
<p>匹配过程(以下匹配过程,如果觉得难以理解,可以暂时跳过,先学会如何使用,再研究为什么可以这样用吧)</p>
<p>首先匹配第一个&ldquo;(&rdquo;,然后一直匹配,直到出现以下两种情况之一:</p>
<p>a)堆栈中Open计数已为0,此时再遇到&ldquo;)&rdquo;</p>
<p>b)匹配到字符串结束符</p>
<p>这时控制权交给(?(Open)(?!)),判断Open是否有匹配,由于此时计数为0,没有匹配,那么就匹配&ldquo;no&rdquo;分支,由于这个条件判断结构中没有&ldquo;no&rdquo;分支,所以什么都不做,把控制权交给接下来的&ldquo;\)&rdquo;</p>
<p>如果上面遇到的是情况a),那么此时&ldquo;\)&rdquo;可以匹配接下来的&ldquo;\)&rdquo;,匹配成功;如果上面遇到的是情况b),那么此时会进行回溯,直到&ldquo;\)&rdquo;匹配成功为止,否则报告整个表达式匹配失败。</p>
<p>由于.NET中的狭义平衡组&ldquo;(?&lt;Close-Open&gt;Expression)&rdquo;结构,可以动态的对堆栈中捕获组进行计数,匹配到一个开始标记,入栈,计数加1,匹配到一个结束标记,出栈,计数减1,最后再判断堆栈中是否还有Open,有则说明开始和结束标记不配对出现,不匹配,进行回溯或报告匹配失败;如果没有,则说明开始和结束标记配对出现,继续进行后面子表达式的匹配。</p>
<p>需要对&ldquo;(?!)&rdquo;进行一下说明,它属于顺序否定环视,完整的语法是&ldquo;(?!Expression)&rdquo;。由于这里的&ldquo;Expression&rdquo;不存在,表示这里不是一个位置,所以试图尝试匹配总是失败的,作用就是在Open不配对出现时,报告匹配失败。</p>
<h2>3、平衡组的应用及优化</h2>
<p>平衡组提供了嵌套结构的匹配功能,这一创新是很让人兴奋的,因为此前正则对于嵌套结构的匹配是无能为力的。然而功能的强大,自然也带来了实现的复杂,正则书写得不好,可能会存在效率陷阱,甚至导致程序崩溃,这里介绍一些基本的优化方法。</p>
<h3>3.1单字符嵌套结构平衡组优化</h3>
<p>单字符的嵌套结构指的是开始和结束标记都单个字符的嵌套结构,这种嵌套相对来说比较简单,优化起来也比较容易。先从上面提到的例子开始。</p>
<h4>3.1.1贪婪与非贪婪模式</h4>
<p>上面给的例子是一种做了部分优化的常规写法,算作是版本1吧,它做了哪些优化呢,先来看下完全没有做过优化的版本0吧。</p>
<div class="jb51code"><pre class="brush:csharp;">string test = "a+(b*(c+d))/e+f-(g/(h-i))*j";
Regex reg0 = new Regex(@"\(                          #普通字符“(”
                            (                       #分组构造,用来限定量词“*”修饰范围
                                (?&lt;Open&gt;\()         #命名捕获组,遇到开括弧Open计数加1
                            |                       #分支结构
                                (?&lt;-Open&gt;\))        #狭义平衡组,遇到闭括弧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))
*/</pre></div>
<p>接下来对比一下版本1。</p>
<div class="jb51code"><pre class="brush:csharp;">Regex reg1 = new Regex(@"\(                          #普通字符“(”
                            (                       #分组构造,用来限定量词“*”修饰范围
                                (?&lt;Open&gt;\()         #命名捕获组,遇到开括弧'Open'计数加1
                            |                       #分支结构
                                (?&lt;-Open&gt;\))        #狭义平衡组,遇到闭括弧'Open'计数减1
                            |                       #分支结构
                                [^()]+              #非括弧的其它任意字符
                            )*                      #以上子串出现0次或任意多次
                            (?(Open)(?!))           #判断是否还有'Open',有则说明不配对,什么都不匹配
                        \)                          #普通闭括弧
                     ", RegexOptions.IgnorePatternWhitespace);</pre></div>
<p>看到区别了吗?版本1对版本0的改进主要有两个地方,一个是用&ldquo;[^()]+&rdquo;来代替&ldquo;.&rdquo;,另一个是用&ldquo;*&rdquo;来代替&ldquo;*?&rdquo;,也就是用贪婪模式来代替非贪婪模式。</p>
<p>如果使用了小数点&ldquo;.&rdquo;,那么为什么不能在分组内使用&ldquo;.+&rdquo;,后面又为什么不能用&ldquo;*&rdquo;呢?只要在上面的正则中使用并运行一下代码就可以知道了,匹配的结果是</p>
<p>(b*(c+d))/e+f-(g/(h-i))</p>
<p>而不是</p>
<p>(b*(c+d))</p>
<p>(g/(h-i))</p>
<p>因为无论是分组内使用&ldquo;.+&rdquo;还是后面使用&ldquo;*&rdquo;,都是贪婪模式,所以小数点会一直匹配下去,直到匹配到字符串的结束符才会停止,然后进行回溯匹配。为了取得正确结果,必须使用非贪婪模式&ldquo;*?&rdquo;。</p>
<p>这就类似于用&ldquo;&rdquo;去匹配&ldquo;(abc)def(ghi)&rdquo;一样,得到的结果是&ldquo;(abc)def(ghi)&rdquo;,而不是通常我们希望的&ldquo;(abc)&rdquo;和&ldquo;(ghi)&rdquo;。这时要用非贪婪模式&ldquo;&rdquo;来得到正确的结果。</p>
<p>贪婪模式和非贪婪模式在匹配失败时,回溯的次数基本上是一样的,效率上没有多大区别,但是在匹配成功时,贪婪模式比非贪婪模式回溯的次数要少得多,效率要高得多。</p>
<p>对于&ldquo;&rdquo;如果既要得到正确的匹配结果,又要提高匹配效率,可以使用排除型捕获组+贪婪模式的方式,即&ldquo;&rdquo;。</p>
<p>版本0的平衡组也是一样,可以使用排除字符组&ldquo;[^()]+&rdquo;和贪婪模式&ldquo;*&rdquo;结合的方式,提高匹配效率,得到的就是版本1的平衡组。</p>
<p>相对于版本0,或许你会认为版本1的写法是很自然的,但是如果不了解这样一个演进过程,那么在字符序列嵌套结构平衡组优化时,就不会是那么自然的一件事了。</p>
<h4>3.1.2 &nbsp;分支结构</h4>
<p>接下来就是分支结构的优化。</p>
<p>语法:(Exp1|Exp2|Exp3)</p>
<p>因为分支结构的匹配规则是,从左向右尝试匹配,当左侧分支匹配成功时,就不再向右尝试。所以使用分支结构时,可以根据以下两条规则进行优化:</p>
<p>1.尽量抽象出每个分支中的公共的部分,使最后的表达式中,每个分支共公部分尽可能的少,比如(this|that)的匹配效率是没有th(is|at)高的。</p>
<p>2. 在不影响匹配结果的情况下,把出现概率高的分支放在左侧,出现概率低的分支放右侧。</p>
<p>对于本例中的分支结构,已经没有公共部分,符合第一条规则,再看下第二条规则,开始标记&ldquo;(&rdquo;和结束标记&ldquo;)&rdquo;出现的概率基本上是一样的,而除&ldquo;(&rdquo;和&ldquo;)&rdquo;之外的字符出现的概率是比&ldquo;(&rdquo;和&ldquo;)&rdquo;出现的概率高的,所以应该把&ldquo;[^()]+&rdquo;分支放在左侧。</p>
<p>版本1由于采用了排除型捕获组,所以这三个分支没有包含关系,左右顺序对结果不会造成影响,可以调整顺序。因为这是已经经过优化的了,而如果是版本0,由&ldquo;.&rdquo;对&ldquo;(&rdquo;和&ldquo;)&rdquo;有包含关系,就不能调整顺序了。</p>
<p>在版本1基础上对分支结构进行优化后,就得到版本2。</p>
<div class="jb51code"><pre class="brush:csharp;">string test = "a+(b*(c+d))/e+f-(g/(h-i))*j";
Regex reg2 = new Regex(@"\(                          #普通字符“(”
                            (                       #分组构造,用来限定量词“*”修饰范围
                                [^()]+              #非括弧的其它任意字符
                            |                       #分支结构
                                (?&lt;Open&gt;\()         #命名捕获组,遇到开括弧Open计数加1
                            |                       #分支结构
                                (?&lt;-Open&gt;\))        #狭义平衡组,遇到闭括弧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))
*/</pre></div>
<h4>3.1.3 &nbsp;捕获组</h4>
<p>这里面主要涉及到了两个捕获组&ldquo;(?&lt;Open&gt;\()&rdquo;和&ldquo;(?&lt;-Open&gt;\))&rdquo;,而在平衡组的应用中,我是只关心它是否匹配了,而对于匹配到的内容是不关心的。对于这样一种需求,可以用以下方式实现</p>
<p>\( (?&lt;Open&gt;)</p>
<p>\)(?&lt;-Open&gt;)</p>
<p>&ldquo;(?&lt;Open&gt;)&rdquo;和&ldquo;(?&lt;-Open&gt;)&rdquo;这两种方式只是使用了命名捕获组,捕获的是一个位置,它总是能够匹配成功的,而匹配的内容是空的,分配的内存空间是固定的,可以有效的节省资源,这在单字符嵌套结构中并不明显,但是在字符序列嵌套结构中就比较明显了。</p>
<p>由于捕获组是直接跟在开始或结束标记之后的,所以只要开始或结束标记匹配成功,命名捕获组自然就会匹配成功,对于功能是没有任何影响的。</p>
<p>那么把标记和捕获组调整一下顺序是否可以呢?从功能上来讲,是可以的,但是匹配的流程上会有所不同,先是捕获组匹配成功,入栈,然后再匹配标记,成功则继续匹配,不成功则该分支匹配失败,进行回溯,出栈,继续尝试下一分支。这样将增加许多入栈和出栈的操作,对匹配效率是有影响的,所以这种方式并不可取。</p>
<p>在版本2基础上对捕获组进行优化后,就得到版本3。</p>
<div class="jb51code"><pre class="brush:csharp;">string test = "a+(b*(c+d))/e+f-(g/(h-i))*j";
Regex reg3 = new Regex(@"\(                          #普通字符“(”
                            (                       #分组构造,用来限定量词“*”修饰范围
                                [^()]+              #非括弧的其它任意字符
                            |                       #分支结构
                                \(  (?&lt;Open&gt;)       #命名捕获组,遇到开括弧Open计数加1
                            |                       #分支结构
                                \)  (?&lt;-Open&gt;)      #狭义平衡组,遇到闭括弧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))
*/</pre></div>
<h4>3.1.4 固化分组</h4>
<p>看到有些人使用平衡组时用到了固化分组,但并不是所有人都明白固化分组的作用。</p>
<p>语法:(?&gt;Expression)</p>
<p>用&ldquo;&rdquo;去匹配&ldquo;(abc)&rdquo;是可以匹配成功的,因为不用回溯,相对于&ldquo;&rdquo;这种非贪婪模式,效率上有所提升,但是对于匹配失败的情况又如何呢?</p>
<p>源字符串:(abc</p>
<p>正则表达式:</p>
<p>匹配中间过程这里不再详述,可以参考NFA引擎匹配原理。</p>
<p>当&ldquo;[^()]+&rdquo;匹配到结束位置时,控制权交给&ldquo;\)&rdquo;,匹配失败,进行回溯,而由于前面使用了&ldquo;[^()]+&rdquo;这种排除型字符组,所以可供回溯的位置,不会存在可以匹配&ldquo;\)&rdquo;的情况,这时候的回溯是完全没有意义的,只会浪费时间,但是由于传统NFA引擎的特点,必须回溯所有可能之后才会报告匹配失败。</p>
<p>这时可以用固化分组来进行优化,一旦占有字符,就不再释放。也就是一旦占有,就不再记录可供回溯的可能。通常是与排除型字符组或顺序否定环视一起使用的。</p>
<p>优化后的正则表达式:</p>
<p>需要说明的一点,固化分组要作用于量词修饰的子表达式才有意义,对于&ldquo;(?&gt;abc)&rdquo;由于内容是固定的,根本就不会产生回溯,所以使用固化分组是没有意义的。</p>
<p>对于平衡组的应用也是一样,如果分组构造中没有量词,那么使用固化分组就是没有意义的,比如版本0</p>
<p>Regex reg = new Regex(@&quot;)|.)*?(?(Open)(?!))\)&quot;);</p>
<p>这种场景下使用固化分组就是没有意义的。</p>
<p>在版本3基础上对捕获组进行优化后,就得到版本4。</p>
<div class="jb51code"><pre class="brush:csharp;">string test = "a+(b*(c+d))/e+f-(g/(h-i))*j";
Regex reg4 = new Regex(@"\(                          #普通字符“(”
                            (?&gt;                     #分组构造,用来限定量词“*”修饰范围
                                [^()]+              #非括弧的其它任意字符
                            |                       #分支结构
                                \(  (?&lt;Open&gt;)       #命名捕获组,遇到开括弧Open计数加1
                            |                       #分支结构
                                \)  (?&lt;-Open&gt;)      #狭义平衡组,遇到闭括弧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))
*/</pre></div>
<p>那么对于分组构造外层的&ldquo;*&rdquo;修饰的子表达式是否可以使用固化分组呢?答案是否定的,因为平衡组通常是要进行回溯才能最终匹配成功的,所以如果使用固化分组,不记录回溯可能的话,将无法得到正确结果。</p>
<h4>3.1.5 &nbsp;进一步优化讨论</h4>
<p>那么现在是不是已经完成优化了呢?是的,通常可以这么认为。在一般应用当中,这已经是从正则层面上来说,最优方案了。</p>
<p>但是在有些场景下,由于Compiled模式可以有效提高分支结构的匹配效率,所以对于源字符串比较复杂的情况,牺牲一些编译时间和内存,还是可以有效提高匹配效率的。</p>
<div class="jb51code"><pre class="brush:csharp;">Regex reg5 = new Regex(@"\(                         #普通字符“(”
                            (?&gt;                     #分组构造,用来限定量词“*”修饰范围
                                [^()]+              #非括弧的其它任意字符
                            |                       #分支结构
                                \(  (?&lt;Open&gt;)       #命名捕获组,遇到开括弧Open计数加1
                            |                       #分支结构
                                \)  (?&lt;-Open&gt;)      #狭义平衡组,遇到闭括弧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))
*/</pre></div>
<p>并不是所有应用场景都适合使用Compiled模式,比如上面这个例子里的源字符串如果是&ldquo;a+(b*(c+d))/e+f-(g/(h-i))*j&rdquo;,本身是非常简单的,使用Compiled模式将是得不偿失的。什么时候使用,要根据具体问题具体分析。</p>
<h3>3.2 字符序列嵌套结构平衡组应用</h3>
<p>字符序列嵌套结构的匹配,典型的应用就是html标签的提取。由于上面详细说明了单字符嵌套结构的优化过程,这里主要讲应用场景,个别涉及到优化的地方再讨论。</p>
<p>字符序列嵌套结构的匹配,举例来说,取div标签。源字符串如下:</p>
<div class="jb51code"><pre class="brush:xhtml;">&lt;div id="0"&gt;
    0
&lt;/div&gt;
&lt;div id="1"&gt;
    1
    &lt;div id="2"&gt;
        2
&lt;/div&gt;
&lt;/div&gt;</pre></div>
<h4>3.2.1提取最外层嵌套结构</h4>
<p>提取最外层div标签,分析过程及构造方式与单字符嵌套结构差不多,只是捕获组等内容稍稍复杂点,先给出实现,再进行解释。</p>
<div class="jb51code"><pre class="brush:csharp;">string test = @"&lt;div id=""0""&gt;
    0
&lt;/div&gt;
&lt;div id=""1""&gt;
    1
    &lt;div id=""2""&gt;
        2
    &lt;/div&gt;
&lt;/div&gt;";
Regex reg = new Regex(@"(?isx)                      #匹配模式,忽略大小写,“.”匹配任意字符
                      &lt;div[^&gt;]*&gt;                      #开始标记“&lt;div...&gt;”
                          (?&gt;                         #分组构造,用来限定量词“*”修饰范围
                              &lt;div[^&gt;]*&gt;  (?&lt;Open&gt;)   #命名捕获组,遇到开始标记,入栈,Open计数加1
                          |                           #分支结构
                              &lt;/div&gt;  (?&lt;-Open&gt;)      #狭义平衡组,遇到结束标记,出栈,Open计数减1
                          |                           #分支结构
                              (?:(?!&lt;/?div\b).)*      #右侧不为开始或结束标记的任意字符
                          )*                          #以上子串出现0次或任意多次
                          (?(Open)(?!))               #判断是否还有'OPEN',有则说明不配对,什么都不匹配
                      &lt;/div&gt;                          #结束标记“&lt;/div&gt;”
                      ");
MatchCollection mc = reg.Matches(test);
foreach (Match m in mc)
{
richTextBox2.Text += m.Value + "\n--------------------\n";
}
/*--------输出--------
&lt;div id="0"&gt;
    0
&lt;/div&gt;
--------------------
&lt;div id="1"&gt;
    1
    &lt;div id="2"&gt;
        2
    &lt;/div&gt;
&lt;/div&gt;
--------------------
*/</pre></div>
<p>在单字符嵌套结构中,使用排除型字符组&ldquo;[^()]+&rdquo;,与分组构造外的匹配优先量词&ldquo;*&rdquo; 达到贪婪模式匹配效果。在字符序列嵌套结构中,要排除的是一个子串,而不是简单的几个无序字符,所以不能使用排除型字符组,此时需要用到顺序否定环视来达到这一目的。&ldquo;(?:(?!&lt;/?div\b).)*&rdquo;表示的是所在位置右侧不是&ldquo;&lt;div&hellip;&gt;&rdquo;或&ldquo;&lt;/div&gt;&rdquo;的字符,这样的字符重复0次或任意多次。关于环视的细节,可以参考 正则基础之&mdash;&mdash;环视。</p>
<p>而由于这种否定环视包含两种状态,所以在与固化分组结合使用时,会与后面的开始或结束标记形成包含关系,所以与固化分组一起使用时,不能放在左侧,只能放在右侧。</p>
<h4>3.2.2 &nbsp;根据id提取div嵌套标签</h4>
<p>根据id提取div时,改变的只是最外层div的结构,对内分组构造内部结构没有影响。但是因为id是变化的,所以正则需要动态生成。下面给出实现,源字符串和输出结果由于比较影响篇幅,就不再给出了。</p>
<div class="jb51code"><pre class="brush:csharp;">string id = Regex.Escape(textBox1.Text);                    //动态获取id
Regex reg = new Regex(@"(?isx)
                      &lt;div(?:(?!(?:id=|&lt;/?div\b)).)*id=(['""]?)" + id  + @"\1[^&gt;]*&gt;        #开始标记“&lt;div...&gt;”
                          (?&gt;                         #分组构造,用来限定量词“*”修饰范围
                              &lt;div[^&gt;]*&gt;  (?&lt;Open&gt;)   #命名捕获组,遇到开始标记,入栈,Open计数加1
                          |                           #分支结构
                              &lt;/div&gt;  (?&lt;-Open&gt;)      #狭义平衡组,遇到结束标记,出栈,Open计数减1
                          |                           #分支结构
                              (?:(?!&lt;/?div\b).)*      #右侧不为开始或结束标记的任意字符
                          )*                          #以上子串出现0次或任意多次
                          (?(Open)(?!))               #判断是否还有'OPEN',有则说明不配对,什么都不匹配
                      &lt;/div&gt;                          #结束标记“&lt;/div&gt;”
                     ");
MatchCollection mc = reg.Matches(test);
foreach (Match m in mc)
{
     richTextBox2.Text += m.Value + "\n--------------------\n";
}</pre></div>
<p>在动态生成正则表达式时,由于输入的字符串中可能存在正则中有特殊意义的元字符,如果不进行转义的话,正则解析时会抛出异常。所以用Regex.Escape(string str)来对动态输入的字符串进行转义处理,确保不会因动态输入的内容而抛异常。比如上面的例子,如果id不进行转义处理时,输入&ldquo;abc(def&rdquo;就会抛&ldquo;) 不足&rdquo;这样的异常。</p>
<h4>3.2.3 根据id提取任意嵌套标签</h4>
<p>再扩展一下,根据id属性取任意嵌套标签。实现如下,具体实现细节和讨论参考 就是通过id获得一个html标签块。以下正则相对于帖子对个别细节做了调整。</p>
<div class="jb51code"><pre class="brush:csharp;">string html = @"
&lt;html&gt;
&lt;body&gt;
&lt;div id=""div1""&gt;
    &lt;div id=""div2"" style=""background:Red;""&gt;
        &lt;div id=""div3""&gt;
            &lt;table id=""table1""&gt;
                &lt;tr&gt;
                    &lt;td&gt;
                        &lt;div id=""div4"" style=""width:100px""&gt;&lt;/div&gt;
                    &lt;/td&gt;
                &lt;/tr&gt;
            &lt;/table&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div id=div5&gt;
        &lt;a href=""http://www.csdn.net""&gt;csdn&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;img src=""http://www.csdn.net/Images/logo_csdn.gif""/&gt;
&lt;/body&gt;
&lt;/html&gt;";
Console.WriteLine(html);
string[] idList = { "div1", "div2", "div3", "div4", "table1", "div5", "abc(def" };
string pattern = @"&lt;(+)(?:(?!\bid\b)[^&lt;&gt;])*id=([""']?){0}\2[^&gt;]*&gt;(?&gt;&lt;\1[^&gt;]*&gt;(?&lt;o&gt;)|&lt;/\1&gt;(?&lt;-o&gt;)|(?:(?!&lt;/?\1).)*)*(?(o)(?!))&lt;/\1&gt;";
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();</pre></div>
<h4>3.2.4 根据标签取外层嵌套结构</h4>
<p>根据动态输入的tag,取相应的最外层的嵌套标签,实现如下。</p>
<div class="jb51code"><pre class="brush:csharp;">string html = @"
&lt;html&gt;
&lt;body&gt;
&lt;div id=""div1""&gt;
    &lt;div id=""div2"" style=""background:Red;""&gt;
        &lt;div id=""div3""&gt;
            &lt;table id=""table1""&gt;
                &lt;tr&gt;
                    &lt;td&gt;
                        &lt;div id=""div4"" style=""width:100px""&gt;&lt;/div&gt;
                    &lt;/td&gt;
                &lt;/tr&gt;
            &lt;/table&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div id=div5&gt;
        &lt;a href=""http://www.csdn.net""&gt;csdn&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;img src=""http://www.csdn.net/Images/logo_csdn.gif""/&gt;
&lt;/body&gt;
&lt;/html&gt;";
Console.WriteLine(html);
string[] tagList = { "html", "body", "div", "table", "abc(def" };
string pattern = @"(?isx)
                      &lt;({0})\b[^&gt;]*&gt;                  #开始标记“&lt;tag...&gt;”
                          (?&gt;                         #分组构造,用来限定量词“*”修饰范围
                              &lt;\1[^&gt;]*&gt;  (?&lt;Open&gt;)    #命名捕获组,遇到开始标记,入栈,Open计数加1
                          |                           #分支结构
                              &lt;/\1&gt;  (?&lt;-Open&gt;)       #狭义平衡组,遇到结束标记,出栈,Open计数减1
                          |                           #分支结构
                              (?:(?!&lt;/?\1\b).)*       #右侧不为开始或结束标记的任意字符
                          )*                          #以上子串出现0次或任意多次
                          (?(Open)(?!))               #判断是否还有'OPEN',有则说明不配对,什么都不匹配
                      &lt;/\1&gt;                           #结束标记“&lt;/tag&gt;”
                     ";
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();</pre></div>
<h4>3.2.5 条件判断结构扩展应用</h4>
<p>条件判断结构的作用不只限于验证开始和结束标记是否配对,根据需求的不同,还可以有其它一些应用。比如在匹配div标签时,只取内部&ldquo;存在&rdquo;嵌套的外层标签。</p>
<div class="jb51code"><pre class="brush:csharp;">string test = @"&lt;div id=""0""&gt;
    0
&lt;/div&gt;
&lt;div id=""1""&gt;
    1
    &lt;div id=""2""&gt;
        2
    &lt;/div&gt;
&lt;/div&gt;";
Regex reg = new Regex(@"(?isx)                              #匹配模式,忽略大小写,“.”匹配任意字符
                      &lt;div[^&gt;]*&gt;                              #开始标记“&lt;div...&gt;”
                          (?&gt;                                 #分组构造,用来限定量词“*”修饰范围
                              &lt;div[^&gt;]*&gt;  (?&lt;Open&gt;)(?&lt;Mask&gt;)  #遇到开始标记,入栈,Open和Mask计数各加1
                          |                                   #分支结构
                              &lt;/div&gt;  (?&lt;-Open&gt;)              #遇到结束标记,出栈,Open计数减1
                          |                                   #分支结构
                              (?:(?!&lt;/?div\b).)*              #右侧不为开始或结束标记的任意字符
                          )*                                  #以上子串出现0次或任意多次
                          (?(Open)(?!))(?(Mask)|(?!))         #'OPEN'保证标记配对,'Mask'保证内部有嵌套
                      &lt;/div&gt;                                  #结束标记“&lt;/div&gt;”
                      ");
MatchCollection mc = reg.Matches(test);
foreach (Match m in mc)
{
     richTextBox2.Text += m.Value + "\n--------------------\n";
}
/*--------输出--------
&lt;div id="1"&gt;
    1
    &lt;div id="2"&gt;
        2
    &lt;/div&gt;
&lt;/div&gt;
--------------------
*/</pre></div>
<p>命名捕获组&ldquo;(?&lt;Mask&gt;)&rdquo;只入栈不出栈,如果内部有嵌套,则&ldquo;(?&lt;Mask&gt;)&rdquo;一定有匹配,此时匹配&ldquo;(?(Mask)yes|no)&rdquo;中的&ldquo;yes&rdquo;子表达式,也就是什么都不做;如果内部没有嵌套,则&ldquo;(?&lt;Mask&gt;)&rdquo;没有匹配,此时匹配&ldquo;(?(Mask)yes|no)&rdquo;中的&ldquo;no&rdquo;子表达式,也就是报告匹配失败。这里省略的是&ldquo;(?(Mask)yes|no)&rdquo;中的&ldquo;yes&rdquo;子表达式。</p>
<p>对于匹配内部没有嵌套的标签,也就是最内层标签,可以使用上面的正则表达式,将&ldquo;(?(Mask)yes|no)&rdquo;中的&ldquo;yes&rdquo;子表达式设为&ldquo;(?!)&rdquo;,将&ldquo;yes&rdquo;子表达式省略。不过这样做有些浪费,完全可以用顺序否定环视来实现这一需求。</p>
<div class="jb51code"><pre class="brush:csharp;">string test = @"&lt;div id=""0""&gt;
    0
&lt;/div&gt;
&lt;div id=""1""&gt;
    1
    &lt;div id=""2""&gt;
        2
    &lt;/div&gt;
&lt;/div&gt;";
Regex reg = new Regex(@"(?is)&lt;div[^&gt;]*&gt;(?:(?!&lt;/?div\b).)*&lt;/div&gt;");
MatchCollection mc = reg.Matches(test);
foreach (Match m in mc)
{
      richTextBox2.Text += m.Value + "\n--------------------\n";
}
/*--------输出--------
&lt;div id="0"&gt;
    0
&lt;/div&gt;
--------------------
&lt;div id="2"&gt;
        2
    &lt;/div&gt;
--------------------
*/</pre></div>
<h2>4、平衡组应用范围探讨</h2>
<p>平衡组可以用来匹配嵌套结构,这是一个很大的创新,但是否就认为平衡组适合用来解决任何嵌套问题呢?事实当然不会是这样。</p>
<p>比如下面这个需求,(参考 请问一个正则表达式) :</p>
<p>源字符串:1+Sum(1,Sum(2, Sum(3), 4), 5)*4+5+Sum(9,Sum(8, Sum(7), 6), 5)*6+7</p>
<p>要求输出:</p>
<p>Sum(1,Sum(2, Sum(3), 4), 5)</p>
<p>Sum(2, Sum(3), 4)</p>
<p>Sum(3)</p>
<p>Sum(9,Sum(8, Sum(7), 6), 5)</p>
<p>Sum(8, Sum(7), 6)</p>
<p>Sum(7)</p>
<p>这种需求使用平衡组+递归的方式可以实现,实现代码如下:</p>
<div class="jb51code"><pre class="brush:csharp;">//递归方法
private void getNesting(string src, Regex reg, List&lt;string&gt; 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&lt;string&gt; list = new List&lt;string&gt;();
Regex reg = new Regex(@"(?i)Sum(?&lt;-o&gt;))*(?(o)(?!))\)", RegexOptions.Compiled);
getNesting(test, reg, list);
foreach (string s in list)
{
     richTextBox2.Text += s + "\n";
}</pre></div>
<p>平衡组虽然可以实现要求,但除非你对效率没有要求,否则这一类需求通常是不适合用正则来实现的。因为平衡组并不是为这一功能而设计的,在实现过程中做了很多额外的尝试。效率上自然要大打折扣。</p>
<p>类似这样的需求,可以自己写有穷自动机来实现,毕竟正则也只不过是一种有穷自动机的实现而已。</p>
<div class="jb51code"><pre class="brush:csharp;">            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&lt;StringBuilder&gt; list = new List&lt;StringBuilder&gt;();
            List&lt;string&gt; groups = new List&lt;string&gt;();
            int level = 0;
            int state = 0;
            foreach (char c in test)
            {
                if ((c == 'S' || c == 's') &amp;&amp; state == 0)
                {
                    state = 1;
                    nesting.Append(c);
                }
                else if ((c == 'U' || c == 'u') &amp;&amp; state == 1)
                {
                    state = 2;
                    nesting.Append(c);
                }
                else if ((c == 'M' || c == 'm') &amp;&amp; state == 2)
                {
                    state = 3;
                    nesting.Append(c);
                }
                else if (c == '(' &amp;&amp; state == 3)
                {
                    state = 0;
                    level++;
                }
                else
                {
                    state = 0;
                    nesting = new StringBuilder(64);
                }
                if (c == ')')
                {
                    if (level &gt; 0)
                    {
                        level--;
                        groups.Add(list.ToString() + c);
                        list.Remove(list);
                    }
                }
                if (level &gt; 0)
                {
                    while(list.Count &lt; level)
                    {
                        list.Add(nesting);
                    }
                    for (int i = 0; i &lt; level; i++)
                    {
                        list.Append(c);
                    }
                }
            }
            foreach (string s in groups)
            {
                Console.WriteLine(s);
            }
            Console.ReadLine();</pre></div>
<h2>5、其它声明</h2>
<p>到此为止,平衡组的基本应用场景和性能调优都已讨论完了,本文对于平衡组匹配原理讲得相对比较少,以应用场景分析为主。主要是因为能够使用平衡组来解决问题的人,通常已经对正则的基本语法有了一定程度的理解。而如果事实确实如此,那么对于平衡组的理解,也是水到渠成的了。</p>
<p>以上正则实现中,采用的多是宽松排列模式,主要是为了加注释,使得阅读清晰。而宽松排列模式通常用于教学目的,实际使用过程中,如果不是为了可读性的考虑,可以去掉这些注释和宽松排列模式参数。</p>
<p>上面给出了很多平衡组的应用,这里需要说明的是,我提供的只是一些方法和思路,从来不推荐把正则当作模板来用,虽然有些时候,它确实可以当作模板来用,但我还是希望你能真正的掌握这些语法规则之后,再去应用平衡组。当然,如果你认为能用就行,不需要知道为什么可以这样用,只是把它当作模板来套,我也无话可说。</p>
頁: [1]
查看完整版本: .NET正则基础之平衡组