C# 篇基础知识8——正则表达式

正则表达式(Regular Expression)也叫匹配模式(Pattern),用来检验字符串是否满足特定规则,或从字符串中捕获满足特定规则的子串。正则表达式的命名空间是System.Text.RegularExpressions,Regex类即正则表达式。

1.字符匹配

最简单的正则表达式由“普通字符”和“通配符”组成。比如“Room\d\d\d”就这样的正则表达式。

这些特殊字符在正则表达式中称为元字符,因为符号“.”在正则表达式里已有特殊用处,所以要想表达“.”本身,需使用它的转移字符“\.”,要表达符号“\”本身,同样,需使用它的转移字符“\\”。 .NET 提供了一批与正则表达式相关的类,它们都位于System.Text.RegularExpressions命名空间,现在我们来学习最主要的Regex 类,并用它来匹配正则表达式。Regex 类的部分方法如表所示:

通过 Regex.Matches()方法,可以从给定字符串中分解出所有与正则表达式匹配的子串,这些子串被保存在一个MatchCollection 型集合中,每个子串都被看作Match 类的对象。

现在假设某份电子文件里包含着 Kitty 的房间号码(格式为RoomXXX),档案很长,人工查阅很耗时间,那么如何通过计算机帮我们找到房间号码呢?

string text = "The Colony is a beautiful town...Kitty lives in Room415...";

//正则表达式

Regex expression = new Regex(@"Room\d\d\d");

//找出所有匹配的字符串,放入集合中。

MatchCollection matches = expression.Matches(text);

//输出匹配的字符串

foreach (Match match in matches)

{ Console.WriteLine("She lives in {0}", match); }

@前缀和转义字符,前面学习过控制文本格式的转义字符,如“\n”、“\"”、“\t”、“\\”等;现在我们又学习了正则表达式的转义字符,如“\.”、“\w”、“\d” 、“\s” 、“\\”等。在正则表达式中它们两者是有区别的。类似Regex expression = new Regex("\d");这样的语句是错误的,因为反斜杠“\”本身就是一个很特殊的字符,要想表示反斜杠本身,需要使用它的转移字符“\\”,所以需要写成下面的形式:Regex expression = new Regex("\\d");但这种形式会降低可读性,所以我们通常使用添加@前缀的方式。Regex expression = new Regex(@"\d");这时会忽略控制文本格式的转义字符,但不忽略正则表达式的转义字符。添加前缀@后,如果字符串里需要双引号本身,可以用两个连续的双引号表示。Regex expression = new Regex(@"Say ""Hello"" to Heaven");。

2.可选的字符集

 

通符符限定了某个位置上匹配的某一类型字符,但有时还想缩小匹配的范围,限定到某一类型的某一范围,这里可以把某个位置上允许出现的字符写在方括号[]内,组成可选字符集,比如:

[abc]表示该位置可以出现字母a、b、c

[A-D]表示该位置可以出现字母A、B、C、D

[A-DM-P]表示该位置可以出现字母A 到D 或M 到P

[A-Da-d]表示该位置可以出现字母A 到D 或a 到d

[12] 表示该位置可以出现数字1 或数字2

[1-5]表示该位置可以出现数字1 到5

[0-57-9]表示该位置可以出现除6 外的所有数字

[\s\S]表示该位置可以出现任何字符,包括任何可见字符和不可见字符(如空格、制表符、换行等)

注意,不管中括号看起来有多长,它都只代表一个字符。表达式中的[VR]、[a-z]、[89]、[0-9]都是一个字符,不能看作多个字符。例如:

string text = "Vitor-1970 Verne-1982 Regan-1998 Robin2008";

//正则表达式

Regex expression = new Regex(@"[VR][a-z][a-z][a-z][a-z]-19[89][0-9]");

//获取并输出匹配的字符串

foreach(Match match in expression.Matches(text))

{ Console.WriteLine(match); }

因为在中括号表达式中,“[”标志着中括号表达式的开始,而“]”标志着中括号表达式的结束,所以要想在中括号表达式外表示它本身,需使用它的转义字符“\[”、 “\]”,同样“-”表示连字符,要想在中括号表达式外表示它本身,需使用它们的转义字符“\-”。对于“-”还有不用转义字符的方式,也能使中括号包含“-”,例如把连字符放在中括号列表的开始或结尾,比如[-1-9]或[abc-];

反向字符集,在中括号表达式用符号“^”表示非,例如[^x]表示匹配除x的所有字符,[^abc] 匹配除a、b、c 以外的所有字符。当然,当需要匹配它本身时,要在中括号表达式中使用它的转义字符“\^”。

3.或匹配| 

在正则表达式中用符号“|”表示“或”,其左右供部分与|一起应放到()中。例如:

"x|y" 匹配 "x"或"y"

"good|ok" 匹配 "good"或"ok"

"(tr|b)ee" 匹配 "tree"或"bee"

"th(i|a)nk" 匹配 "think"或"thank"

"Book One|Two" 匹配 "Book One"或"Two"

"Book (One|Two)" 匹配 "Book One"或"Book Two"

因为符号“|”、“(”和“)”在正则表达式中也有特殊用处,所以当需要匹配它们本身时,也要使用它们的转义字符“\|”、“\(”“\)”。

4.数量限定符

数量限定符有多种,它们之间有细微的区别。

数量限定符“*”将前面的字符重复0 次或多次,而数量限定符“+”将前面的字符重复1 次或多次,数量限定符“?”将前面的字符重复0 次或1 次,如果需要明确指定重复次数,可使用数量限定符{n},它表示把前面的字符重复n 遍,而数量限定符{n,}表示把它前面的字符至少重复n遍, 数量限定符{n,m}把前面的字符重复n至m遍。例如:

string words = "lg log loog looog loooog looooog";

//正则表达式

Regex expression = new Regex("lo*g");

//获取并输出匹配的字符串

foreach (Match match in expression.Matches(words))

{ Console.WriteLine(match); }

 

Regex expression1 = new Regex("lo+g");

//获取并输出匹配的字符串

foreach (Match match in expression1.Matches(words))

{ Console.WriteLine(match); }

 

Regex expression2 = new Regex("lo?g");

//获取并输出匹配的字符串

foreach (Match match in expression2.Matches(words))

{ Console.WriteLine(match); } 

//正则表达式

Regex expression = new Regex("lo{3}g");

//获取并输出匹配的字符串

foreach (Match match in expression.Matches(words))

{ Console.WriteLine(match);}

 

//正则表达式

Regex expression = new Regex("lo{3,}g");

//获取并输出匹配的字符串

foreach (Match match in expression.Matches(words))

{ Console.WriteLine(match);} 

//正则表达式

Regex expression = new Regex("lo{2,4}g");

//获取并输出匹配的字符串

foreach (Match match in expression.Matches(words))

{ Console.WriteLine(match);} 

以上4个输出结果分别为:“lg loog looog loooog looooog”、 “log loog looog loooog looooog”、“lg log”、“looog”、“looog  loooog  looooog”、“loog looog loooog”。

用数量限定符还可以重复多个字符,例如:

string words = "lg leog leoeog leoeoeog leoeoeoeog leoeoeoeoeog";

//正则表达式

Regex expression = new Regex("l(eo)+g");

//获取并输出匹配的字符串

foreach (Match match in expression.Matches(words))

{ Console.WriteLine(match); }

正则表达式“l(eo)+g”表示字符串的第一个字符为l,最后一个字符为g,中间有一个或多个eo。结果为:"leog leoeog leoeoeog leoeoeoeog leoeoeoeoeog"。

最奇妙的是这些数量限定符还可以与通配符组合。比如通配符“\w”匹配任意单词字符,限定符“*”表示把前面的字符重复0 次或多次,所以正则表达式“\w*”匹配由任意单字符组成的任意长度的单词。例如:

string girls = @"Van is 16; Vicky is 18; Vivien is 19; Vinvcent is 22";

Regex expression = new Regex(@"V\w* is \d\d");

foreach (Match match in expression.Matches(girls))

{ Console.WriteLine(match); }

结果为:Van is 16 、Vicky is 18、Vivien is 19、Vinvcent is 22

贪婪和懒惰 

以上限定符都是“贪婪的”(Greedy),它们会匹配尽可能多的文本。如果在限定符后加上?号,它就会变成“懒惰的”(Lazy),会匹配尽可能少的文本。

string words = "ab<H1>Hello World</H1>c";

//贪婪的限定符

Regex expression1 = new Regex("<.*>");

MatchCollection matchs1 = expression1.Matches(words);

Console.WriteLine("There is {0} match with greedy quantifier:",matchs1.Count);

foreach (Match match in matchs1)

{Console.WriteLine(match); }

//懒惰的限定符

Regex expression2 = new Regex("<.*?>");

MatchCollection matchs2 = expression2.Matches(words);

Console.WriteLine("There are {0} matchs with lazy quantifier:",matchs2.Count);

foreach (Match match in matchs2)

{Console.WriteLine(match);}

使用贪婪的限定符有一条长子串与之匹配,使用懒惰的限定符有两条短子串与之匹配。所以结果分别为:<H1>Hello World</H1>、<H1>  </H1>。。一个是“{n}?”,它实际上与“{n}”是等价的,因为不管是贪婪还是懒惰,都已经限定死了,只能重复n次。“?”是把“?”前面字母重复0 到1 次。因为“?”是贪婪的,能重复1 次就不重复0 (实在没有也能匹配),“??”是懒惰的,重复次数要尽量少,所以它选择重复0 次。

符号“*”、“+”、“?”“{”和“}”在正则表达式中也有特殊用处,所以当需要

匹配它们本身时,也要使用它们的转义字符“\*”、“\+”、“\?”“\{”和“\}”。

5.定位符

通过定位符可以在指定位置寻找匹配的子串。若正则表达式中使用了定位符“^”,则在整个字符串的头部寻找匹配的子串。例如string words = "year1998 year2008 year2018"; Regex expression = new Regex(@"^year\d\d\d\d");“^”匹配字符串的开头位置,所以“^year\d\d\d\d”表示要从字符串的开头位置开始寻找,这里匹配结果为year1998。若正则表达式中使用了定位符“$”,则在整个字符串的尾部寻找匹配的子串。string words = "year1998 year2008 year2018"; Regex expression = new Regex(@"year\d\d\d\d$");匹配结果为year2018。

若正则表达式中使用了定位符“\b”,则在字符串中每个“单词”的边界寻找匹配的子串。(单词通常以空格、段首、段尾、逗号、句号等符号作为边界,分隔符“-”也可以作为边界。但要注意“\b”只表示单词与边界符号之间的“位置”,不表示边界符号本身。)

string words = "formfordfork";

Console.Write("None anchors:");

Regex expression = new Regex(@"for\w");

foreach (Match match in expression.Matches(words))

{ Console.Write(match + "\t"); }

Console.Write("\nAnchor start:");

expression = new Regex(@"\bfor\w");

foreach (Match match in expression.Matches(words))

{ Console.Write(match + "\t"); }

Console.Write("\n Anchor end:");

expression = new Regex(@"for\w\b");

foreach (Match match in expression.Matches(words))

{ Console.Write(match + "\t");}

结果为:form ford fork 、form、fork。

可以看出当没有定位符时,匹配单词中所有符合要求的子串;当定位符\b 在前面时,在单词的头部寻找匹配的子串;当定位符\b 在后面时,在单词的结尾寻找匹配的子串。常用正则表达式“\b\w+\b”找出一句话里的所有单词

string words = "are you ok?";

Regex expression = new Regex(@"\b\w+\b");

foreach (Match match in expression.Matches(words))

{Console.WriteLine(match);}

结果为:are you ok。

显然,“^”和“$”在正则表达式中也有特殊用处,所以当要匹配它们本身时也需使用它们的转义字符“\^”和“\$”。

6.分组和向后引用

括号表达式并不简单的起着确定范围的作用,它同时会创建子表达式,每个子表达式形成一个分组,并把捕获到的与子表达式匹配的子串保存在分组中,以供将来使用。电子邮件地址通常由用户名、二级域名、一级域名三部分构成,通常用"(\w+)@(\w+)\.(\w+)"匹配,这里有三个括号,形成三个子表达式,就会出现三个分组。默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,按分组左括号的出现顺序进行编号,第一个分组的组号为1,第二个为2,以此类推。在正则表达式里引用分组的语法为“\number”,比如“\1”代表与分组1 匹配的子串,“\2”代表与分组2 匹配的字串,等等。存储起来的分组有什么作用呢?下面我们通过一个例子来说明。很多句子里会有重复的单词,下面我们就通过正则表达式找出这些重复的单词。

string text = "you are very very good";

foreach (Match match in Regex.Matches(text, @"\b(\w+)\b \1\b"))

{ Console.WriteLine(match);}

结果为:very very。

前面已经知道“\b(\w+)\b”匹配一个单词,因为这里面有一个括号,所以会捕获一个以1 为组号的分组,后面的“\1”就代表这个分组,表示这个位置上出现和分组1 一样的内容。所以两者连起来就表示两个重复的单词。这种在后面的表达式中引用前面的分组的方式就叫做后向引用。除了匹配重复的单词,后向引用还有一个较为常见的应用,那就是匹配有效的HTML标签。例如:

string words = "<h0>not valid</h1> <h2>valid</h2> <h3>not valid</h4>";

foreach (Match match in Regex.Matches(words, @"<(.*?)>.*?</\1>"))

{Console.WriteLine(match);}

结果为:<h2>valid</h2>。

7.替换文本

正则表达式除了查找文本外,另一项重要功能就是替换文本。通过Regex 类的Replace()方法就能以特定字符串替换原字符串中与正则表达式匹配的子串。下面通过一个例子说明如何替换文本以及如何在替换中引用分组。电话号码格式(010)88665987、(0769)23658945 ,可以用正则表达式“\((\d{3,4})\)(\d{7,8})”匹配,该表达式中有两个分组,第一个分组对应区号,第二个分组对应号码,现在来把电话本中所有的电话都改为形如010-88665987 的格式。

string phonebook = "(010)88665987 (020)23658945 (021)88965222";

string pattern = @"\((\d{3,4})\)(\d{7,8})";

string result = Regex.Replace(phonebook, pattern, "$1-$2");

Console.WriteLine(result);

结果:010-88665987、020-23658945、021-88965222。

在大部分语言的正则表达式中,查找时,使用后向引用的语法为“\number”;而在替换时,其语法为“$number”。 例子中的Regex.Replace ()方法把与正则表达式匹配的子串替换为“$1-$2”,其中$1 对应与分组1(即区号),$2 对应分组2(即电话号码)。

在.NET 中使用正则表达式进行替换时,分组的命名方式为:(?<name> subexpression)(即可以对分组显式命名),后向引用的语法是:${name}。所以上例也可以写为:

string phonebook = "(010)88665987 (020)23658945 (021)88965222";

string pattern = @"\((?<areacode>\d{3})\)(?<number>\d{8})";

string result = Regex.Replace(phonebook, pattern, "${areacode}-${number}");

Console.WriteLine(result);

8. 非捕获分组和预查

 

1)非捕获后组?:

很多时候,我们添加一个分组,并不是为了在后向引用中使用它,而是出于表达上的需要。比如正则表达式“(tr|b)ee”中括号,仅仅是为了确定范围,使之正确匹配“tree”或“bee”。 存储分组会花费一定的时间,如果我们不需要存储分组,可以在分组内部添加元字符“?:”,把它标志成一个非捕获分组。"(?:tr|b)ee"。这样就不会存储分组了。

2)正向预查?=和负正向预查?!

string text = @"Jack boy 29 010-88127631 Microsoft, Sally girl 22 010-88127632 Microsoft,Ben boy 26 020-65423541 Google,Merry girl 23 020-65423542 Google,Alan boy 27 021-23456851 Apple,Kitty girl 18 021-23456852 Apple.";

Console.WriteLine("The Telephone numbers of Microsoft are:");

foreach (Match match in Regex.Matches(text, @"\d{3}-\d{8}(?= Microsoft)"))

{Console.WriteLine(match);}

结果为:010-88127631、010-88127632。

如果仅仅使用正则表达式“\d{3}-\d{8}”,它就会匹配文本里所有的电话号码,那么如何让它只匹配微软的电话号码呢?这时就需要在它后面的一个分组中添加元字符“(?= Microsoft)”作为限制条件,其格式如下所示。正向预查表达式用来筛选与查询目标表达式匹配的文本,其结果不光要和查询目标表达式匹配,它后面跟随的文本还要和正向预查表达式匹配。需要强调的是正向预查表达式只是用来筛选查询目标,并不属于查询目标本身,不包含在最终结果中。

负正向预查与正向预查相反,查询目标后面不能跟指定的字符串,其语法为在分组中添加元字符“?!”。 正则表达式“\d{3}-\d{8}(?! Microsoft)”匹配那些后面不跟“ Microsoft”的电话号码。

3)反向预查?<=和负反向预查?<!

正向预查?=筛选查寻目标,限制条件在查询目标的后面,反向预查?<=筛选查寻目标时,限制条件则在查询目标的前面,这时就需要在它前面的一个分组中添加元字符“(?= Microsoft)”作为限制条件。其使用方法例如:

string text = @"Jack boy 29 010-88127631 Microsoft, Sally girl 22 010-88127632 Microsoft,Ben boy 26 020-65423541 Google,Merry girl 23 020-65423542 Google,Alan boy 27 021-23456851 Apple,Kitty girl 18 021-23456852 Apple.";

Console.WriteLine("The Telephone numbers of girls are:");

foreach (Match match in Regex.Matches(text, @"(?<=girl \d\d )\d{3}-\d{8}"))

{Console.WriteLine(match);}

负反向预查与反向预查相反,查询目标前面不能跟指定字符串,其语法是在分组中添加元字符“?<!”。正则表达式“(?<!girl \d\d )\d{3}-\d{8}”匹配这样的电话号码,它们前面的文本不能与“girl \d\d ”匹配。

总之,预查表达式的作用就是给查询目标添加限制条件,可以在前面,也可以在后面,可以正面限制,也可以反面限制。

9.正则表达式的类

和正则表达式相关的类都在命名空间System.Text.RegularExpressions 中,上图展示了部分常用正则表达式类。

string s = "aaa@163.com bbb@263.net ccc@363.cn";

MatchCollection matchs = Regex.Matches(s, @"(\w+)@(\w+)\.(\w+)");

//输出MatchCollection中所有的Match

Console.WriteLine("所有Match为:");

for (int i = 0; i < matchs.Count; i++)

{ Match match = matchs[i];

Console.WriteLine(match.Value); }

Macth 的Value 属性就是匹配的字符串,每个 Match 中都有一个Groups 属性,里面包含了该Match 的所有分组。其中第0 个分组是Match 本身

10.正则表达式的选项

通过Regex 类的Options 属性可以设置正则表达式的选项,下表是.Net 中常用的正则表达式选项。

例如:

string pattern = @"Room\d{3}";

Regex expression = new Regex(pattern, RegexOptions.IgnoreCase);

如果想同时设置多个选项,可以把它们用按位或“|”连接起来。

new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);

11.注释

遇到复杂的、比较难理解的正则表达式时,我们可以通过添加注释的方式进行解释说明。在正则表达式中添加注释的语法为:(?#注释内容)。比如:

@"\((\d{3,4})(?#该分组匹配区号)\)(\d{7,8})(?#该分组匹配电话号码)"。 

 

posted @ 2019-05-15 10:24  Hellozhu  阅读(2214)  评论(0编辑  收藏  举报