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})(?#该分组匹配电话号码)"。