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))
因为无论是分组内使用“.+”还是后面使用“*”,都是贪婪模式,所以小数点会一直匹配下去,直到匹配到字符串的结束符才会停止,然后进行回溯匹配。为了取得正确结果,必须使用非贪婪模式“*?”。
这就类似于用“\(.+\)<
分享到:
相关推荐
本文件“正则基础之——捕获组(capture group)”将深入讲解正则表达式中的一个重要概念——捕获组,这是理解和运用正则表达式不可或缺的一部分。 捕获组是正则表达式中的一种机制,它允许我们将一个模式分隔成多...
#### 正则表达式基础 正则表达式是一种强大的文本匹配工具,它允许你查找字符串中的模式,并可以用于替换、提取等操作。对于提取 HTML 中的图片路径,我们需要设计一个能够匹配 `<img>` 标签并捕获 `src` 属性值的...
DotNet正则表达式测试工具,基于.net1.1开发,vista以上操作系统绿色免安装。 用于编写正则表达式时进行测试,支持正则选项(是否忽略大小写、是否多行模式)、支持正则替换
《.NET正则表达式测试工具深度解析》 在编程领域,正则表达式(Regular Expression,简称regex)是一种强大的文本处理工具,广泛应用于数据验证、字符串搜索与替换等多个场景。.NET框架提供了丰富的正则表达式支持...
本文件“正则基础之——NFA引擎匹配原理.rar”将深入探讨NFA的工作机制。 首先,我们需要理解NFA的基本概念。NFA是一种有向图,每个节点代表一个状态,边则表示状态间的转换。在NFA中,一个输入字符可以引发多个...
Asp.net 正则表达式 正则 正则表达式 正则表达式下载 正则下载 Asp.net 正则表达式下载
- **平衡组/递归匹配**:处理嵌套结构的复杂匹配任务。 - 示例: - 使用递归模式匹配嵌套括号。 #### 四、实战应用举例 - **简单匹配**:如`hi`匹配字符串`hi`。 - **单词边界匹配**:如`\bhi\b`精确匹配单词`hi`...
在.NET框架中,正则表达式是一种强大的文本处理工具,用于模式匹配和字符串操作。通过学习和理解正则表达式,开发者能够更高效地处理各种复杂的文本匹配任务。这篇文档旨在30分钟内引导初学者快速入门正则表达式,并...
本文将深入探讨“正则基础之——环视”这一主题,旨在帮助你理解和掌握正则表达式中的环视技术。 环视(Lookaround)是正则表达式中的一个重要概念,它允许我们在不包含在最终匹配结果中的情况下,检查某个位置的...
在这个"正则基础之——小数点"的主题中,我们将深入探讨小数点在正则表达式中的应用。 1. 小数点(`.`)的元字符含义: 在正则表达式中,小数点(`.`)是一个元字符,它代表任意单个字符,除了换行符。这意味着`.`...
在深入探讨如何在ASP.NET中使用正则表达式来匹配特定格式的汉字字符串之前,我们首先需要理解题目中提到的正则表达式:“^20\\d{2}年[第]{1}[0-9]{3}期\(总[第]{1}[0-9]{3}期\)$”。这个表达式旨在精确匹配类似于...
正则获取 正则获取所有 正则替换 正则截取等等一些方法封装,直接调用就可以了
在本基础教程中,我们将探讨如何使用.NET正则表达式进行字符串操作,特别是在Visual Studio 2013环境下。 首先,我们要理解字符组(Character Groups)。字符组允许我们指定一组字符,正则表达式将在匹配时考虑这组...
首先,让我们探讨一下.NET正则表达式的基础知识。在.NET中,正则表达式是通过`System.Text.RegularExpressions`命名空间中的`Regex`类实现的。这个类提供了一系列的方法,如`Match`、`Matches`和`Replace`,用于执行...
《.NET正则表达式测试台:高效开发与调试利器》 正则表达式是编程领域中用于处理字符串的强大工具,广泛应用于数据验证、文本提取、搜索替换等多个场景。在.NET框架中,正则表达式提供了丰富的功能和高度的灵活性。...
正则基础之——NFA引擎匹配原理 在正则表达式中,了解引擎匹配原理是非常重要的。就像音乐家一样,一个人可以演奏出动听的乐曲,但是如果不知道如何去改变音符的组合,乐曲就不会变得更动听。同样,在使用正则...
《.Net正则表达式测试工具深度解析》 在编程领域,正则表达式是一种强大的文本处理工具,广泛应用于字符串的查找、替换和提取。在.NET框架中,正则表达式提供了一套全面且强大的API,使得开发者能够方便地进行各种...
《.NET正则表达式(网页版)》是针对ASP.NET和C#开发者的一个资源,旨在帮助他们理解和掌握正则表达式这一强大的文本处理工具。正则表达式(Regular Expression)在编程领域中广泛用于数据验证、搜索与替换等场景,...
regexdesigner.net是一个强力的可视化工具,可以帮助我们构建与测试.NET正则表达式,RegexDesigner.NET让我们将我们开发出的正则表达式集成到我们的应用程序,可以方便的生成C#或VB.NET代码并编译成程序集 ...