你的位置:首页 > ASP.net教程

[ASP.net教程][.net 面向对象程序设计进阶] 正则表达式 (二) 高级应用


[.net 面向对象程序设计进阶] (2) 正则表达式 (二)  高级应用

   上一节我们说到了C#使用正则表达式的几种方法(Replace,Match,Matches,IsMatch,Split等),还有正则表达式的几种元字符及其应用实例,这些都是学习正则表达式的基础。本节,我们继续深入学习表达式的几种复杂的用法。

 1.分组 

用小括号来指定子表达式(也叫做分组) 

我们通过前一节的学习,知道了重复单个字符,只需要在字符后面加上限定符就可以了, 

比如 a{5},如果要重复多个字符,就要使用小括号分组,然后在后面加上限定符,下面我们看一个示例。 

示例一:重复单字符 和 重复分组字符

//示例:重复单字符 和 重复分组字符//重复 单个字符Console.WriteLine("请输入一个任意字符串,测试分组:");string inputStr = Console.ReadLine();string strGroup1 = @"a{2}";Console.WriteLine("单字符重复2两次替换为22,结果为:"+Regex.Replace(inputStr, strGroup1,"22"));//重复 多个字符 使用(abcd){n}进行分组限定string strGroup2 = @"(ab\w{2}){2}";Console.WriteLine("分组字符重复2两次替换为5555,结果为:" + Regex.Replace(inputStr, strGroup2, "5555"));

运行结果如下:

示例二:校验IP4地址(如:192.168.1.4,为四段,每段最多三位,每段最大数字为255,并且第一位不能为0) 

//示例:校验IP4地址(如:192.168.1.4,为四段,每段最多三位,每段最大数字为255,并且第一位不能为0)string regexStrIp4 = @"^(((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?))$";Console.WriteLine("请输入一个IP4地址:");string inputStrIp4 = Console.ReadLine();Console.WriteLine(inputStrIp4 + " 是否为合法的IP4地址:" + Regex.IsMatch(inputStrIp4, regexStrIp4));Console.WriteLine("请输入一个IP4地址:");string inputStrIp4Second = Console.ReadLine();      Console.WriteLine(inputStrIp4 + " 是否为合法的IP4地址:" + Regex.IsMatch(inputStrIp4Second, regexStrIp4));

 运行结果如下:

在这个正则表达式中,我们需要理解 2[0-4]\d|25[0-5]|[01]?\d\d?

这部分表示一个三位数的三种形式

2[0-4]\d     表示:2开头+0~4的一位数字+任意数

25[0-5]      表示:25开头+0~5的一位数字

[01]?\d\d?  表示:0或1或空  +  一位任意数字  +  一位任意数或空

这三种形式使用|分开,表示择一种匹配

理解了这段,整体就理解了,前半部分为 重复三次带.号,后面一个不带.号

^ 和 $ 表示匹配整体的开始和结束

2. 后向引用

在这节之前,还是比较好理解的,这段有点难以理解了,但是我们还得拍拍脑袋继续学习。

先不说后向引用是啥,我们先说,后向引用能干啥?

前面我们说了分组的概念,就是在正则表达式中使用小括号()完成分组,后向引用就是针对分组的。

如果前面分组以后,后面还要检索前面分组内同样的内容,就用到了后向引用,意思就是后面引用前面的分组之意。

…………………………………………………………………容我喘口气…………………………………………………………………………………………

那么要如何在后面引用前面分组呢?

这里就需要有一个组名,即定义一个组名,在后面引用。

在定义组名前,说一下

则对于分组的工作原理:

A.分组后,正则会自动为每个分组编一个组号

B.分配组号时,从左往右扫描两遍,第一遍给未命名分组分配组号,第二遍自然是给命名组分配组号。因此,命名组的组号是大于未命名组的组号。(说这些工作原理,对我们使用后向引用没有什么作用,就是理解一下,内部工作原理而已)

C.也不是所有的组都会分配组号,我们可以使用(:?exp)来禁止分配组号。 

不知道小伙伴们是否看明白了,至少我看了网上关于这块儿的介绍,都不是很清晰。

下面先看一下后向引用 捕获 的定义

分类

代码/语法

说明

捕获

(exp)

匹配exp,并捕获文本到自动命名的组里

(?<name>exp)

匹配exp,并捕获文本到名称为name的组里,也可以写成(?'name'exp)

(?:exp)

匹配exp,不捕获匹配的文本,也不给此分组分配组号

 先来对上面的三个语法理解一下:

(exp) 就是分组,使用() ,exp就是分组内的子表达式 ,这个在上面分组中已经说明了

(?<name>exp)或 (?’name’exp) 就是组分组命名,只是有两种写法而已

(?:exp)exp表达式不会被捕获到某个组里,也不会分配组号,但不会改变正则处理方式,肯定很多小盆友要问,为啥要有这个东西,要他何用?

大家看一下下面这个示例:

^([1-9][0-9]*|0)$  表示匹配0-9的整数

再看下面这个

^(?:[1-9][0-9]*|0)$ 也是匹配0-9的整数,但是使用了非捕获组

作用呢?两个字:效率

因为我们定义了组,但是我们只是为了使用组来限定范围开始和结束,并不是为了后向引用,因此使用非捕获组来禁上分配组号和保存到组里,极大的提高了检索效率。

有了上面的理解,我才好写点示例,先理论后实践。 

在进行示例前,我们先复习以下上节学到的元字符

   *    匹配前面的子表达式任意次。例如,zo* 能匹配“z”,“zo”以及“zoo”。*等价于{ 0,}。

   +    匹配前面的子表达式一次或多次(大于等于1次)。例如,“zo +”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{ 1,}。

   ?    匹配前面的子表达式零次或一次。例如,“do (es) ?”可以匹配“do”或“does”中的“do”。?等价于{ 0,1}。  

   \w  匹配包括下划线的任何单词字符。类似但不等价于“[A-Za-z0-9_]”,这里的"单词"字符使用Unicode字符集。

   \b  匹配一个单词边界,也就是指单词和空格间的位置(即正则表达式的“匹配”有两种概念,一种是匹配字符,一种是匹配位置,这里的\b就是匹配位置的)。例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”。       

   \s  匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。

//后向引用//在后向引用前,我们先熟悉一下单词的查找 //复习一下前面学的几个元字符// * 匹配前面的子表达式任意次。例如,zo* 能匹配“z”,“zo”以及“zoo”。*等价于{ 0,}。// + 匹配前面的子表达式一次或多次(大于等于1次)。例如,“zo +”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{ 1,}。// ? 匹配前面的子表达式零次或一次。例如,“do (es) ?”可以匹配“do”或“does”中的“do”。?等价于{ 0,1}。 // \w 匹配包括下划线的任何单词字符。类似但不等价于“[A-Za-z0-9_]”,这里的"单词"字符使用Unicode字符集。// \b 匹配一个单词边界,也就是指单词和空格间的位置(即正则表达式的“匹配”有两种概念,一种是匹配字符,一种是匹配位置,这里的\b就是匹配位置的)。例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”。    // \s 匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。string str1 = "zoo zoo hello how you mess ok ok home house miss miss yellow";//示例一:查找单词中以e结尾的,并将单词替换为了*string regexStr1 = @"\w+e\b";Console.WriteLine("示例一:" + Regex.Replace(str1, regexStr1,"*"));//示例二:查询单词中以h开头的,并将单词替换为#string regexStr2 = @"\bh\w+";Console.WriteLine("示例二:" + Regex.Replace(str1, regexStr2, "#"));//示例三:查找单词中有重叠ll的单词,将单词替换为@string regexStr3 = @"\b\w+l+\w+\b";Console.WriteLine("示例三:" + Regex.Replace(str1, regexStr3, "@"));

此外,我们说到了分组及编号,那么如何取到某个分组的匹配文本呢?

使用 \1 表示取第一个分组的匹配文本 

//使用 \1 代表第一个分组 组号为1的匹配文本//示例四:查询单词中有两个重复的单词,替换为% string regexStr4 = @"(\b\w+\b)\s+\1";Console.WriteLine("示例四:" + Regex.Replace(str1, regexStr4, "%"));//上面示例中,第一个分组匹配任意一个单词 \s+ 表示任意空字符 \1 表示匹配和第一个分组相同

 别名定义 (?<name>exp)  ,别名后向引用 \k<name> 

//使用别名 代替 组号 ,别名定义 (?<name>exp) ,别名后向引用 \k<name>//示例五:使用别名后向引用 查询 查询单词中有两个重复的单词,替换为%% string regexStr5 = @"(?<myRegWord>\b\w+\b)\s+\k<myRegWord>";Console.WriteLine("示例五:"+Regex.Replace(str1, regexStr5, "%%"));

 运行结果如下:

3. 零宽断言

什么!?什么!?对你没看错,这段题目叫“零宽断言”(顿时1W头艹尼马奔腾而过!!!!)

本来正则表达式学到本节,小伙伴们渐渐感觉看着有点吃力了,还来这么一个名字。

不过,不要忘记,我们这一系列的文章叫“.NET 进阶”既然是进阶,自然就有很多有难度的问题要解决,我们还是先看一下这个概念的意思吧!

什么是零宽断言?

我们有经常要查询某些文本某个位置前面或后面的东西,就像 ^  $  \b一样,指定一个位置,这个位置需要满足一定的条件(断言),我们把这个称做 零宽断言 

分类

代码/语法

说明

零宽断言

(?=exp)

匹配exp前面的位置

(?<=exp)

匹配exp后面的位置

(?!exp)

匹配后面跟的不是exp的位置

(?<!exp)

匹配前面不是exp的位置

看上面的表,零宽断言一共有四个类型的位置判断写法

我们先举几个示例来看看,TMD零宽断言到底解决了什么问题?

//零宽断言//示例一:将下面字符串每三位使用逗号分开string stringFist = "dfalrewqrqwerl43242342342243434abccc";string regexStringFist = @"((?=\w)\w{3})";string newStrFist = String.Empty;Regex.Matches(stringFist, regexStringFist).Cast<Match>().Select(m=>m.Value).ToList<string>().ForEach(m => newStrFist += (m+","));Console.WriteLine("示例一:" + newStrFist.TrimEnd(','));//示例二:查询字符串中,两个空格之间的所有数字string FindNumberSecond = "asdfas 3 dfasfas3434324 9 8888888 7 dsafasd342";string regexFindNumberSecond = @"((?<=\s)\d(?=\s))";string newFindNumberSecond = String.Empty;Regex.Matches(FindNumberSecond, regexFindNumberSecond).Cast<Match>().Select(m => m.Value).ToList<string>().ForEach(m => newFindNumberSecond += (m + " "));Console.WriteLine("示例二:" + newFindNumberSecond);

运行结果如下:

?=exp)也叫零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式exp。

(?<=exp)也叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式exp。

4.  负向零宽断言

实在无语了,零宽断言刚理解完,又来了一个负向零宽断言,真是非常的坑爹。

解释这个概念已经非常无力了,除非你不是地球人。但这个东东本身并不难使用,我们直接解决一个问题。

//负向零宽断言//示例:查找单词中包含x并且x前面不是o的所有单词string strThird = "hot example how box house ox xerox his fox six my";string regexStrThird = @"\b\w*[^o]x\w*\b";string newStrThird = String.Empty;Regex.Matches(strThird, regexStrThird).Cast<Match>().Select(m => m.Value).ToList<string>().ForEach(m => newStrThird += (m + " "));Console.WriteLine("示例一:" + newStrThird);//我们发现以上写法,如果以x开头的单词,就会出错,原因是[^o]必须要匹配一个非o的字符//为了解决以上问题,我们需要使用负向零宽断言 (?<!o])负向零宽断言能解决这样的问题,因为它只匹配一个位置,并不消费任何字符//改进以后如下string regexStrThird2 = @"\b\w*(?<!o)x\w*\b";string newStrThird2 = String.Empty;Regex.Matches(strThird, regexStrThird2).Cast<Match>().Select(m => m.Value).ToList<string>().ForEach(m => newStrThird2 += (m + " "));Console.WriteLine("示例二:" + newStrThird2);//示例三:如查询上面示例,但是要求区配必须含o但 后面不能有x 则要使用 (?!x)string regexStrThird3 = @"\b\w*o(?!x)\w*\b";string newStrThird3 = String.Empty;Regex.Matches(strThird, regexStrThird3).Cast<Match>().Select(m => m.Value).ToList<string>().ForEach(m => newStrThird3 += (m + " "));Console.WriteLine("示例三:" + newStrThird3);

运行结果如下:

5. 注释

正则表达式很长的时候,很难读懂,因些他有专门的注释方法,主要有两种写法

A.通过语法(?#comment) 

例如:\b\w*o(?!x)(?#o后面不包含x)\w*\b  红色部分是注释,他不对表达式产生任何影响

B.启用“忽略模式里的空白符”选项的多行注释法

例如:

//正则表达式注释//重写上面的例子,采用多行注释法string regexStrThird4 = @"\b     #限定单词开头             \w*    #任意长度字母数字及下划线             o(?!x)   #含o字母并且后面的一个字母不是x             \w*    #任意长度字母数字及下划线             \b     #限定单词结尾             ";string newStrThird4 = String.Empty;Regex.Matches(strThird, regexStrThird4,RegexOptions.IgnorePatternWhitespace).Cast<Match>().Select(m => m.Value).ToList<string>().ForEach(m => newStrThird4 += (m + " "));Console.WriteLine("示例四:" + newStrThird4);

运行结果同上。

6. 注释贪婪与懒惰

这个比较好理解,首先,懒惰是针对重复匹配模式来说的,我们先看一下下面的表 懒惰的限定符如下:

懒惰限定符

代码/语法

说明

*?

重复任意次,但尽可能少重复

+?

重复1次或更多次,但尽可能少重复

??

重复0次或1次,但尽可能少重复

{n,m}?

重复n到m次,但尽可能少重复

{n,}?

重复n次以上,但尽可能少重复

 通过示例来看

//懒惰限定符string LazyStr = "xxyxy";string regexLazyStr = @"x.*y";string regexLazyStr2 = @"x.*?y";string newLazyStr = String.Empty;Regex.Matches(LazyStr, regexLazyStr).Cast<Match>().Select(m => m.Value).ToList<string>().ForEach(m => newLazyStr += (m + " "));string newLazyStr2 = String.Empty;Regex.Matches(LazyStr, regexLazyStr2).Cast<Match>().Select(m => m.Value).ToList<string>().ForEach(m => newLazyStr2 += (m + " "));Console.WriteLine("贪婪模式:" + newLazyStr);Console.WriteLine("懒惰模式:" + newLazyStr2);

运行结果如下:

我们可以看出:

懒惰匹配,也就是匹配尽可能少的字符。

相反,贪婪模式则尽可能多的匹配字符

贪婪模式都可以被转化为懒惰匹配模式,只要在它后面加上一个问号? 

7.处理与选项

常用的处理选项

名称

说明

IgnoreCase(忽略大小写)

匹配时不区分大小写。

Multiline(多行模式)

更改^和$的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$的精确含意是:匹配\n之前的位置以及字符串结束前的位置.)

Singleline(单行模式)

更改.的含义,使它与每一个字符匹配(包括换行符\n)。

IgnorePatternWhitespace(忽略空白)

忽略表达式中的非转义空白并启用由#标记的注释。

ExplicitCapture(显式捕获)

仅捕获已被显式命名的组。

 这个我们在前面已经很多次用到了,比如 注释的示例。

8.平衡组/递归匹配

随后补充。待续。 

==============================================================================================  

返回目录

 <如果对你有帮助,记得点一下推荐哦,有不明白的地方或写的不对的地方,请多交流>
  

<对本系列文章阅读有困难的朋友,请先看《.net 面向对象编程基础》>

<转载声明:技术需要共享精神,欢迎转载本博客中的文章,但请明版权及URL>

QQ群:467189533 

==============================================================================================