正则基础之——反向引用

1          概述

捕获组捕获到的内容,不仅可以在正则表达式外部通过程序进行引用,也可以在正则表达式内部进行引用,这种引用方式就是反向引用。要了解反向引用,首先要了解捕获组,关于捕获组,参考 正则基础之 —— 捕获组( capture group

反向引用的作用通常是用来查找或限定重复、查找或限定指定标识配对出现等等。

对于普通捕获组和命名捕获组的引用,语法如下:

普通捕获组反向引用: /k<number> ,通常简写为 /number

命名捕获组反向引用: /k<name> 或者 /k'name'

普通捕获组反向引用中 number 是十进制的数字,即捕获组的编号;命名捕获组反向引用中的 name 为命名捕获组的组名。

2        反向引用匹配原理

捕获组 (Expression) 在匹配成功时,会将子表达式匹配到的内容,保存到内存中一个以数字编号的组里,可以简单的认为是对一个局部变量进行了赋值,这时就可以通过反向引用方式,引用这个局部变量的值。一个捕获组 (Expression) 在匹配成功之前,它的内容可以是不确定的,一旦匹配成功,它的内容就确定了,反向引用的内容也就是确定的了。

反向引用必然要与捕获组一同使用的,如果没有捕获组,而使用了反向引用的语法,不同语言的处理方式不一致,有的语言会抛异常,有的语言会当作普通的转义处理。

2.1      从一个简单例子说起

源字符串: abcdebbcde

正则表达式: ([ab])/1

对于正则表达式“ ([ab])/1 ”,捕获组中的子表达式“ [ab] ”虽然可以匹配“ a ”或者“ b ”,但是捕获组一旦匹配成功,反向引用的内容也就确定了。如果捕获组匹配到“ a ”,那么反向引用也就只能匹配“ a ”,同理,如果捕获组匹配到的是“ b ”,那么反向引用也就只能匹配“ b ”。由于后面反向引用“ /1 ”的限制,要求必须是两个相同的字符,在这里也就是“ aa ”或者“ bb ”才能匹配成功。

考察一下这个正则表达式的匹配过程,在位置 0 处,由“ ([ab]) ”匹配“ a ”成功,将捕获的内容保存在编号为 1 的组中,然后把控制权交给“ /1 ”,由于此时捕获组已记录了捕获内容为“ a ”,“ /1 ”也就确定只有匹配到“ a ”才能匹配成功,这里显然不满足,“ /1 ”匹配失败,由于没有可供回溯的状态,整个表达式在位置 0 处匹配失败。

正则引擎向前传动,在位置 5 之前,“ ([ab]) ”一直匹配失败。传动到位置 5 处时,,“ ([ab]) ”匹配到“ b ”,匹配成功,将捕获的内容保存在编号为 1 的组中,然后把控制权交给“ /1 ”,由于此时捕获组已记录了捕获内容为“ b ”,“ /1 ”也就确定只有匹配到“ b ”才能匹配成功,满足条件,“ /1 ”匹配成功,整个表达式匹配成功,匹配结果为“ bb ”,匹配开始位置为 5 ,结束位置为 7

扩展一下,正则表达式“ ([a-z])/1{2} ”也就表达连续三个相同的小写字母。

2.2      一个复杂例子的分析

详细的分析讨论参考: 正则表达式正向预搜索的问题

源字符串: aaa bbbb ffffff 999999999

正则表达式: (/w)((?=/1/1/1)(/1))+

测试代码:

string test = "aaa bbbb ffffff 999999999" ;

Regex reg = new Regex (@"(/w)((?=/1/1/1)(/1))+" );

MatchCollection mc = reg.Matches(test);

foreach (Match m in mc)

{

      richTextBox2.Text += " 匹配结果:" + m.Value.PadRight(12, ' ' ) + " 匹配开始位置:" + m.Index + "/n" ;

}

// 输出

匹配结果 bb          匹配开始位置 4

匹配结果:ffff        匹配开始位置:9

匹配结果:9999999     匹配开始位置:16

匹配结果分析:

正则表达式 (/w)((?=/1/1/1)(/1))+ 从匹配结果上分析,其实就等价于 (/w)(/1)*(?=/1/1/1)(/1) ,这个会相对好理解一些,下面讨论下分析过程。

因为“ + ”等价于“ {1,} ”,表示至少匹配 1 次,下面把子表达式“ ((?=/1/1/1)(/1))+ ”展开来看下规律,下表中的 次数 表示子表达式“ ((?=/1/1/1)(/1))+ ”匹配成功的次数

次数

等价表达式

1

(/w)((?=/1/1/1)(/1))

2

(/w)((?=/1/1/1)(/1))((?=/1/1/1)(/1))

3

(/w)((?=/1/1/1)(/1))((?=/1/1/1)(/1))((?=/1/1/1)(/1))

如果最后一个“ ((?=/1/1/1)(/1)) ”匹配成功,那么中间的“ ((?=/1/1/1)(/1)) ”一定可以匹配成功,所以中间的限制条件 (?=/1/1/1) 就没有意义了,这时就可以简写为“ (/1) ”,也就是

次数

等价表达式

1

(/w)((?=/1/1/1)(/1))

2

(/w)(/1)((?=/1/1/1)(/1))

3

(/w)(/1)(/1)((?=/1/1/1)(/1))

可以归纳为等价于

(/w)(/1)*((?=/1/1/1)(/1))

因为“ ((?=/1/1/1)(/1)) ”开始和结尾的 () 原来是用作量词 + 修饰范围的,这里已经没有什么意义了,所以表达式最后可以归纳为等价于

(/w)(/1)*(?=/1/1/1)(/1)

分析这个表达式就容易多了。“ (/w) ”匹配一个字符,占一位,“ /1 ”是对“ /w ”匹配内容的引用,“ (/1)* ”可以匹配 0 到无穷多个“ (/w) ”匹配到的字符,“ (?=/1/1/1)(/1) ”只占一位,但是“ (?=/1/1/1) ”要求所在位置右侧有三个连续相同的“ (/w) ”匹配到的字符,所以在“ (?=/1/1/1) ”这个位置右侧应该有三个字符,不过只有这个位置右侧的一个字符计入最后的匹配结果,最后两个只作为限制条件,不计入最后的匹配结果

以“ 999999999 ”为例,第一个“ 9 ”由“ (/w) ”匹配,第二到第六个“ 9 ”由“ (/1)* ”来匹配,第七个“ 9 ”由“ (?=/1/1/1)(/1) ”中最后的“ (/1) ”来匹配,而第七、八、九这三个“ 9 ”是用来保证满足“ (?=/1/1/1) ”这个条件的。

2.3      反向引用的编号

对于普通捕获组的反向引用,是通过捕获组的编号来实现的,那么对于一些可能存在歧义的语法又是如何解析的呢?对于正则表达式

([ab])/10

这里的“ /10 ”会被解析成第 10 个捕获组的反向引用,还是第 1 个捕获组的反向引用加一个普通字符“ 0 ”呢 ? 不同语言的处理方式是不一样的。

string test = "ab0cdebb0cde" ;

richTextBox2.Text = Regex .Match(test, @"([ab])/10" ).Value;

.NET 中,以上测试代码输出为空,说明这里的“ /10 ”被解析成第 10 个捕获组的反向引用,而这个表达式中是不存在第 10 个捕获组的,所以匹配结果为空。

<


script


 


type


=

"text/javascript"

>


 
var


 str = 

"ab0cdebb0cde"


;

var


 reg = /([ab])/

10


/;

var


 arr = str.match(reg);

if


(arr != 

null


)

{

    
document.write(arr[

0


]);

}

</


script


>


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

bb0

*/

而在 JavaScript 中,由于浏览器解析引擎的不同,得到的结果也不一样,以上为 IE 下是可以得到匹配结果“ bb0 ”,说明在 IE 的浏览器引擎中,“ /10 ”被解析成第 1 个捕获组的反向引用加一个普通字符“ 0 ”。而在 Firefox Opera 等浏览器中,得到的结果为空,说明“ /10 ”被解析成第 10 个捕获组的反向引用,而这个表达式中是不存在第 10 个捕获组的。

string test = "ab0cdebb0cde" ;

richTextBox2.Text = Regex .Match(test, @"([ab])/10" , RegexOptions .ECMAScript).Value;

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

bb0

*/

而在 .NET 中,如果正则表达式加了 RegexOptions .ECMAScript 参数,则这里的“ /10 ”被解析成第 1 个捕获组的反向引用加一个普通字符“ 0 ”。

至于正则表达式中确实有 10 个以上的捕获组时,“ /10 ”的具体意义留给有兴趣的读者去测试了,因为在实际应用当中,如果你的正则表达式中用到了 10 个以上捕获组,而同时又用到了第 10 个以上捕获组的反向引用时,就要注意分析一下,你的正则是否需要进行优化,甚至于这里是否适合使用正则表达式了。

出于对现实应用场景的分析,第 10 个以上捕获组的反向引用几乎不存在,对它的研究通常仅存在于理论上。而对于 10 个以内捕获组反向引用后面还有数字,容易造成混淆的情况,可以通过非捕获组来解决。

([ab])/1(?:0)

这样就可以明确,是对第 1 个捕获组的反向引用,后面跟一个普通字符“ 0 ”。也就不会产生混淆了。

string test = "ab0cdebb0cde" ;

richTextBox2.Text = Regex .Match(test, @"([ab])/1(?:0)" ).Value;

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

bb0

*/

而事实上,即使是这样用的场景也非常少,至今为止,只在日期正则表达式中用到过。

^(?:(?!0000)[0-9]{4} ([-/.]?) (?:(?:0?[1-9]|1[0-2]) /1 (?:0?[1-9]|1[0-9]|2[0-8]) |(?:0?[13-9]|1[0-2]) /1 (?:29|30) |(?:0?[13578]|1[02]) /1 (?:31) )|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00) ([-/.]?) 0?2 /2 (?:29) )$

这一节讨论的内容,了解一下就可以了,在实际应用当中,如果遇到,注意一下不要出现混淆而导致匹配结果错误就可以了。

3        反向引用应用场景分析

反向引用的作用通常是用来查找或限定重复、查找或限定指定标识配对出现等等。以下以实例进行场景分析及应用讲解。

3.1      查找重复

查找重复通常的应用场景是查找或验证源字符串中,是否有重复单词、重复项等等。

3.1.1   验证数字元素重复项

需求描述:

验证源字符串中以“ , ”分隔的数字是否有重复项。

代码实现:

string [] test = new string [] { "1,2,3,123,32,13" , "12,56,89,123,56,98" , "8,2,9,10,38,29,2,9" , "8,3,9,238,93,23" };

Regex reg = new Regex (@"/b(/d+)/b.*?/b/1/b" );

foreach (string s in test)

{

     richTextBox2.Text += " 源字符串: " + s.PadRight(20, ' ' ) + " 验证结果: " + reg.IsMatch(s) + "/n" ;

}

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

源字符串: 1,2,3,123,32,13     验证结果: False

源字符串: 12,56,89,123,56,98  验证结果: True

源字符串: 8,2,9,10,38,29,2,9  验证结果: True

源字符串: 8,3,9,238,93,23     验证结果: False

*/

源字符串的规则比较明确,就是用“ , ”分隔的数字,类似于这种查找是否有重复的需求,最简单的就是用反向引用来解决了。

由于要验证的是用“ , ”分隔的元素的整体是否有重复,所以“ (/d+) ”两侧的“ /b ”就是必须的,用来保证取到的数字子串是一个元素整体,而不是“ 123 ”中的“ 1 ”,当然,这里前后两个“ /b ”分别换成“ (?<!/d) ”和“ (?!/d) ”是一个效果,可能意义上更明确。后面的两个“ /b ”也是一样的作用。

3.1.2   验证 连续数字是否有重复

参考 问两个正则表达式

需求描述:

数据:

1985aaa1985bb

bcae1958fiefadf1955fef

atijc1944cvkd

df2564isdjfef2564d

实现1: 匹配第一次出现的四个数字. 然后后面也存在这四个数字的

:

1985aaa1985bb

第一次出现的四个数字是1985. 然后后面也存在这四个数字, 所以这个匹配

bcae1958fiefadf1955fef

第一次出现的四个数字是1958. 然后后面不存在这四个数字. 所以不匹配

-----

所以实现1. 应该匹配

1985aaa1985bb

df2564isdjfef2564d

代码实现:

// 如果是验证第一个出现的连续4 个数字是否有重复

string [] test = new string [] { "1985aaa1985bb" , "bcae1958fiefadf1955fef" , "atijc1944cvkd" , "df2564isdjfef2564d" , "abc1234def5678ghi5678jkl" };

Regex reg = new Regex (@"^(?:(?!/d{4}).)*(/d{4})(?:(?!/1).)*/1" );

foreach (string s in test)

{

     richTextBox2.Text += " 源字符串:  " + s.PadRight(25, ' ' ) + " 验证结果:  " + reg.IsMatch(s) + "/n" ;

}

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

源字符串:  1985aaa1985bb            验证结果:  True

源字符串:  bcae1958fiefadf1955fef   验证结果:  False

源字符串:  atijc1944cvkd            验证结果:  False

源字符串:  df2564isdjfef2564d       验证结果:  True

源字符串:  abc1234def5678ghi5678jkl 验证结果:  False

*/

由于需求要求验证第一次出现的四个数字是否有重复,所以这里需要用“ ^(?:(?!/d{4}).)*(/d{4}) ”来保证捕获组取得的是第一次出现的四个数字。

这样写可能有些复杂,可读性较差,但这里需要用这种顺序环视结合贪婪模式,来达到匹配第一次出现的四个数字的目的,而不能使用非贪婪模式 .

对于使用非贪婪模式的正则“ ^.*?(/d{4})(?:(?!/1).)*/1 ”,可以看一下它匹配的结果。

string [] test = new string [] { "1985aaa1985bb" , "bcae1958fiefadf1955fef" , "atijc1944cvkd" , "df2564isdjfef2564d" , "abc1234def5678ghi5678jkl" };

Regex reg = new Regex (@"^.*?(/d{4})(?:(?!/1).)*/1" );

foreach (string s in test)

{

     richTextBox2.Text += " 源字符串:  " + s.PadRight(25, ' ' ) + " 验证结果:  " + reg.IsMatch(s) + "/n" ;

}

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

源字符串:  1985aaa1985bb            验证结果:  True

源字符串:  bcae1958fiefadf1955fef   验证结果:  False

源字符串:  atijc1944cvkd            验证结果:  False

源字符串:  df2564isdjfef2564d       验证结果:  True

源字符串:  abc1234def5678ghi5678jkl 验证结果:  True

*/

是的,最后一项的验证结果也是“ True ”,为什么会这样?当捕获组“ (/d{4}) ”匹配到“ 1234 ”时,由于“ 1234 ”没有重复,所以后面的子表达式匹配失败,此时“ .*? ”会进行回溯,放弃当前状态,继续向前匹配,直到它匹配到“ 5678 ”前的“ f ”,由捕获组“ (/d{4}) ”匹配到“ 5678 ”,后面的子表达式可以匹配成功,报告整个表达式匹配成功。

NFA 引擎在有可供回溯的状态时,会一直尝试直到所有可能都尝试失败后才报告失败。上例中非贪婪模式在继续尝试时是可以找到匹配成功的位置的,而采用贪婪模式的正则“ ^(?:(?!/d{4}).)*(/d{4}) ”,由于“ ^(?:(?!/d{4}).)* ”匹配到的内容不可能是连续的四个数字,所以无论怎么回溯,接下来的“ (/d{4}) ”都不可能匹配成功,一直回溯到起始位置“ ^ ”,报告整个表达式匹配失败。

而后面的顺序环视 + 贪婪模式子表达式“ (?:(?!/1).)* ”则不存在以上问题,所以在源字符串比较简单时可以写作“ .*? ”,不会影响匹配结果。

而对于验证任意位置是否存在四个重复数字,则不需要加起始位置的限定。

// 如果是验证任意位置出现的连续4 个数字是否有重复,可以用我38 楼的正则

string [] test = new string [] { "1985aaa1985bb" , "bcae1958fiefadf1955fef" , "atijc1944cvkd" , "df2564isdjfef2564d" , "abc1234def5678ghi5678jkl" };

Regex reg = new Regex (@"(/d{4})(?:(?!/1).)*/1" );

foreach (string s in test)

{

     richTextBox2.Text += " 源字符串:  " + s.PadRight(25, ' ' ) + " 验证结果:  " + reg.IsMatch(s) + "/n" ;

}

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

源字符串:  1985aaa1985bb            验证结果:  True

源字符串:  bcae1958fiefadf1955fef   验证结果:  False

源字符串:  atijc1944cvkd            验证结果:  False

源字符串:  df2564isdjfef2564d       验证结果:  True

源字符串:  abc1234def5678ghi5678jkl 验证结果:  True

*/

3.2      限定指定标识配对

相对于查找重复来说,查找或指定标识配对出现这种应用场景要更多一些。尤其是对于 HTML 的处理中,这种应用更普遍。

3.2.1   限定标点配对

由于 HTML 语言的不规范性,导致以下三种写法可以被解析。

1.    <a href=www.csdn.net>CSDN</a>

2.    <a href='www.csdn.net' >CSDN</a>

3.    <a href="www.csdn.net" >CSDN</a>

而这对于一些需要进行字符串解析的应用,造成很大的麻烦。在提取链接时,虽然两侧都用“ [‘”]? ”通常也可以得到正确结果,却不如用反向引用来得严谨、方便。

Regex reg = new Regex (@"(?is)<a(?:(?!href=).)*href=(['""]?)(?<url>[^""'/s>]*)/1[^>]*>(?<text>(?:(?!</a>).)*)</a>" );

MatchCollection mc = reg.Matches(yourStr);

foreach (Match m in mc)

{

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

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

}

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

www.csdn.net

CSDN

www.csdn.net

CSDN

www.csdn.net

CSDN

*/

以下可以正确解析出三种形式的 HTML 代码中的链接和文本,下面把正则改一下

Regex reg = new Regex (@"(?is)<a(?:(?!href=).)*href=(['""])?(?<url>[^""'/s>]*)/1[^>]*>(?<text>(?:(?!</a>).)*)</a>" );

看到区别了吗?只是把“ ([‘””]?) ”改成了“ ([‘””])? ”,结果会怎么样呢?

Regex reg = new Regex (@"(?is)<a(?:(?!href=).)*href=(['""])?(?<url>[^""'/s>]*)/1[^>]*>(?<text>(?:(?!</a>).)*)</a>" );

MatchCollection mc = reg.Matches(yourStr);

foreach (Match m in mc)

{

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

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

}

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

www.csdn.net

CSDN

www.csdn.net

CSDN

*/

结果只取到了两组数据。这是因为对于情况 1 HTML 字符串,在“ ([‘””]?) ”这种情况下,捕获组虽然匹配到的只是一个位置,但毕竟是匹配成功了,所以可以用“ /1 ”进行反向引用;而改成“ ([‘””])? ”,捕获组根本就没有进行匹配,所以也就无法进行反向引用。

当然,对于 HTML 来说,还有一些比较复杂的情况,如

<a href="javascript:alert(1 > 2)"/>

这种复杂情况涉及到的场景比较少,通常应用可以不予以考虑,否则考虑的场景太复杂,会影响匹配效率。写正则的一个一般原则就是,适用就好。这种场景如果遇到,需求根据具体情况,是否需要提取等进行分析,根据分析结果不同,写出的正则也是不一样的。

3.2.2           限定标签配对

这种应用一般是在取某几个特定标签,或是动态生成正则表达式时用到。

需求描述:

删除 <script…>…</script> <style…>…</style> 标签及其中间的内容。

代码实现:

Regex reg = new Regex (@"(?is)<(script|style)/b[^>]*>(?(?!/1/b).)*<//1>" );

string result = reg.Replace(yourStr, "" );

因为这里要删除的标签不止一个,所以事先无法确定是哪个标签,需要用到反向引用来限定标签的配对。

当然,对于标签有嵌套的情况,就要用到平衡组了。可以参考 .NET 正则基础之 —— 平衡组

3.2.3   取配对标签中的内容

需求描述:

[id]5554323[id!][destid]10657302023180404[destid!][srcterminalid]13518841197[srcterminalid!][msgcontent] 好的 [msgcontent!][receivetime]20090409165217[receivetime!]

源字符串中标签成对出现,无嵌套,分别提取标签和对应的内容。

代码实现:

string test = "[id]5554323[id!][destid]10657302023180404[destid!][srcterminalid]13518841197[srcterminalid!][msgcontent] 好的[msgcontent!][receivetime]20090409165217[receivetime!]" ;

Regex reg = new Regex (@"(?s)/[([^/]]+)/]((?:(?!/[/1).)*)/[/1!/]" );

MatchCollection mc = reg.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += "Tag: " + m.Groups[1].Value.PadRight(20, ' ' ) + "Content: " + m.Groups[2].Value + "/n" ;

}

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

Tag: id                  Content: 5554323

Tag: destid              Content: 10657302023180404

Tag: srcterminalid       Content: 13518841197

Tag: msgcontent          Content: 好的

Tag: receivetime         Content: 20090409165217

*/

这种需求通常是由捕获组匹配到一个标签,然后向后匹配,直到与之配对的标签外为止,根据源字符串的特点,中间可以使用非贪婪模式,也可以使用顺序否定环视 + 贪婪模式。

3.3      反向引用的综合应用

3.3.1     12 位数字,其中不能出现 6 位连续相同数字

需求描述:

只允许12 位数字,并且其中不能出现6 位连续相同数字。

例如,123456789012 是允许的,而123333334567 是不允许的。

正则表达式: ^(?:([0-9])(?!/1{5})){12}$

类似这种需要判定是否有连续相同元素的需求,其实也是验证重复,也要用到反向引用。

说下分析过程,需求分解一下:

1、 一个数字

2、 它后面不能连续出现5 个与它相同的数字

3、 满足以上两条的字符一共12

那么根据需求分解写出相应的正则:

1 ([0-9])

2 (?!/1{5})

3 .{12}

将以上三个分解后得出的正则,按需求逻辑关系,组合一下:

(([0-9])(?!/1{5})){12}

由于是验证整个字符串的规则,所以开始和结束标识“ ^ ”和“ $ ”是少不了的,不需要用捕获组的地方,用非捕获代替,也就成了最后满足需求的正则:

^(?:([0-9])(?!/1{5})){12}$

其实这个例子的分析过程,也是一些正则问题解析的通用过程,先把复杂的需求由整到零的分解,再各个实现,然后把实现的正则由零到整,考虑一下相互间的逻辑关系,基本上就可以得出正确的正则表达式了。

3.3.2     A-Z 以内不重复的 10 个字母

需求描述: A-Z 以内不重复的 10 个字母

正则表达式 1 ^(?:([A-Z])(?!.*?/1)){10}$

正则表达式 2 ^(?:([A-Z])(?=((?!/1).)*$)){10}$

这个需求与上一个需求类似,分析过程也差不多。其实这个问题如果用正则来实现,思路是非常清晰的

首先因为是验证规则,所以 “^” “$” 是必不可少的,分别匹配开始和结束的位置

然后是 10 个字母,那么 ([A-Z]){10} ,合起来就是 ^([A-Z]){10}$

最后就是加一个规则,字母不能重复

如何保证不能重复,必然是用到反向引用 ( 一个字母 ) 后面任意一个字母不能与这个字母重复,这样实现起来就有两种方式,当然,实质都是一样的

实现方式一: ^(?:([A-Z])(?!.*?/1)){10}$

实现方式二: ^(?:([A-Z])(?=(?:(?!/1).)*$)){10}$

在这个需求当中,由于可能出现的源字符串不会太长,也不会太复杂,所以这两个正则表达式在匹配效率上不会有明显的差异。

解释一下正则的含义,先解释一下方式一的正则:

^(?:([A-Z])(?!.*?/1)){10}$

^ ”和“ $ ”分别匹配开始和结束位置,“ {10} ”为量词,表示修饰的子串重复 10 次。

(?:Expression) ”是非捕获组,目的是不将“ () ”内的“ Expression ”匹配的内容保存到内存中,之所以要这样用,是因为后面的反向引用使用的是“ /1 ”,如果不用非捕获组,那么“ ([A-Z]) ”就是编号为 2 的捕获组,后面的“ /1 ”就要换成“ /2 ”,来引用第二个捕获组,替换后对匹配结果当然不会有什么影响,但由于由“ (([A-Z])(?!.*?/1)) ”捕获的内容我们并不关心,所以还是用非捕获组,可以提升匹配效率。

([A-Z]) ”就是匹配 A Z 之间的任意一个字母,并保存匹配结果到捕获组中。

(?!.*?/1) ”顺序环视,它是零宽度的,虽然进行匹配,但不保存匹配结果,可以理解为它就是在所在位置的右侧附加了一个条件,用在这里表示,它所在位置的右侧,不管间隔多少个字符,都不能出现之前匹配到的那个字符,也就是不能有重复的字母出现。

(?:([A-Z])(?!.*?/1)){10} ”就是匹配到这样一个字符

1 、它首先是一个字母;

2 、然后这个字母的右侧间隔任意多个字符,不能再出现同样的字母;

3 、最后,符合以上两条规则的字符,一共有 10 个。

加上首尾限定字符“ ^ ”和“ $ ”,就是满足需求的正则。

接下来讨论一下方式二的正则:

^(?:([A-Z])(?=(?:(?!/1).)*$)){10}$

思路和以及其余部分子表达式与方式一完全一样 ,只有“ (?=(?:(?!/1).)*$) ”这里不同,这个子表达式表示,它所在位置右侧,一直到结尾,都不能是之前匹配到的那个字符。方式一是非贪婪模式的实现,而这个就是贪婪模式的实现。

这里需要用到顺序肯定环视“ (?=Expression) ”,而不能用非捕获组“ (?:(?:(?!/1).)*$) ”,是因为这里的表达式不能占有字符,只能作为条件存在,由量词“ {10} ”修饰的子表达式最终只能匹配一个字符,否则就无法限定长度了。

3.3.3     提取指定单元长度字符串

需求描述 参考 求一正则表达式( c#

例如有字符串 string str = " w1w2w3w2w3w1w3w2w4w5w4w5w4w4w5w4w2w4w3w4w3w2w6w5w6w5w6w4w7 " , 找出有且仅有两个单元( w+ 数字作为一个单元,例如: w1,w2 )组成的长度大于等于 4 个单元的字串(必须包括这两个单元),这个例子,应输出: " w2w3w2w3 "," w4w5w4w5w4w4w5w4 "," w4w3w4w3 "," w6w5w6w5w6 "

如果找出有且仅有三个单元长度大于等于 6 个单元的字串,该如何写正则表达式?

代码实现:

// 第一个需求,两单元的

string str = "w7w7w7w5w7w1w2w3w2w3w1w3w2w4w5w4w5w4w4w5w4w2w4w3w4w3w2w6w5w6w5w6w4w7w7w7w5w7" ;

MatchCollection mc = Regex .Matches(str, @"(?i)(?=(w/d)/1*(w/d))(?:/1|/2){4,}" );

foreach (Match m in mc)

{

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

}

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

bb0w7w7w7w5w7

w2w3w2w3

w4w5w4w5w4w4w5w4

w4w3w4w3

w6w5w6w5w6

w4w7w7w7

*/

// 第二个需求,三单元的

string str = "w7w7w7w5w7w1w2w3w2w3w1w3w2w4w5w4w5w4w4w5w4w2w4w3w4w3w2w6w5w6w5w6w4w7w7w7w5w7" ;

MatchCollection mc = Regex .Matches(str, @"(?i)(?=(w/d)/1*(w/d)(?:/1|/2)*(w/d))(?:/1|/2|/3){6,}" );

foreach (Match m in mc)

{

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

}

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

bb0w7w7w7w5w7w1

w2w3w2w3w1w3w2

w4w5w4w5w4w4w5w4w2w4

w2w6w5w6w5w6

w4w7w7w7w5w7

*/

这个实例可以认为是环视和反向引用综合运用的一个经典实例。主要是用到了环视零宽度,不占有字符的特性,先由环视来取得规定单元的捕获组的内容,再通过反向引用来进行实际的匹配。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值