C#实现正则表达式入门实战教程
如果有人和你说,如果不将字符串转换为数字,你要如何判断字符串是否由全数字组成?把字符串拆成char数组,然后放入一个循环,来判断每个char是否为数字?那你要如何判断手机号是否合法?IP是否合法呢?把字符串拆成char数组总不是个办法啊,是否有更好的解决办法?有的,正则表达式就是。正则表达式是什么?可以这么说,它是一种字符串语法,可以形容字符串的格式。本文就来介绍正则表达式,就像我在其他博客中讲过的,我不喜欢把所有规则列出来,规则在网上一搜一大把。我要讲的是正则表达式究竟能够做什么,怎么使用正则表达式。
维基百科中,正则表达式的解释为:正则表达式(英语:Regular Expression、regex或regexp,缩写为RE),也译为正规表示法、常规表示法,在计算机科学中,是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。没错,正则表达式也是一个字符串,只不过这个字符串能够用来判断另一个字符串是否满足一定规则,如是否由全数字组成,是否是合法的手机号码,是否是合法的IP。只要掌握了正则表达式,你就相当于掌握了一种描述正则表达式的语法,当然这种语法不是用来和人交流的,而是和”机器“交流的,你只要打出^((\w)(\w)\1\2)+$机器就能告诉你一个字符串是否长成ababab...这样。下面我来讲解正则表达式。我不会列出所有语法,因为网上很多。
我会以Lesson的形式,先介绍实例,然后详细分析实例,因为正则表达式的规则确实很难记,但是能达到的效果是很好记的,只要你看过有人实现了利用正则表达式来判断一个字符串是否由全数字组成,那么你就再也不会忘了,下次再有类似的要求的时候,你至少知道用正则表达式来做会很简单,那么剩下的就是翻出正则表达式的手册,研究一阵子,自然就出来了,因为虽然规则难记,但是很好理解。
- Lesson1 判断字符串是否由全字符组成。
该题的想法很简单,从”头“到”尾“,全是数字就可以了。打开手册,看到表示开头的字符是^,表示结尾的字符是$,表示数字的字符是\d,然后看到想要匹配多个字符,有两个选择,+和*,+是至少出现一次,*是0次或者多次,^\d+$。这就完成了!好,让我们来试一试。在c#中,正则表达式相关的类是System.Text.RegularExpressions.Regex,其中我用到的方法有Match,Replace,和IsMatch,分别是匹配,替换和判断是否匹配的便捷方法。用法也很简单,看下我的例子就会明白了。
internal static class RegexExtension
{//将匹配信息转换为字符串信息
public static string Convert2String(this Match match, string enter)
{
StringBuilder builder = new StringBuilder();
builder.AppendFormat("匹配的字符串为:{0}, 是否匹配成功{1}, 匹配到的字符串为{2}, 匹配的位置为{3}, 匹配的长度为{4},一共匹配到{5}个结果",
enter, match.Success, match.Value, match.Index, match.Length, match.Groups.Count);
return builder.ToString();
}
}
public static void Lesson1(){
Console.WriteLine("全数字的判断:");
string[] enters = {"123123123", "123a123", "a123123", "", "0 "};
foreach (var enter in enters){
Console.WriteLine(
Regex.Match(enter, @"^\d+$").Convert2String(enter));
}
}
Regex.Match方法会将enter与规则进行匹配,然后将结果转换为字符串,例子中只有 ”123123123“ 会匹配成功,其他都是失败,即使”0 “也会失败,是因为有空格,而\d只会匹配数字。是不是很简单?下面来看第二课。
- Lesson2 判断号码是否为合法的座机或者手机号码
判断号码是否为固话或者手机号,固话是7-8位,手机为11位,这时就需要2种情况都可以匹配,先看固话,全是数字已经会了,是^\d+$ ,只要加上个数的限制就可以了,查看手册,发现{m,n} 描述有几个字符,m表示最少出现的次数,n表示最多出现的次数。那么 +字符就应该是和{1,} 一样,不填n表示不限制最大次数,那么m不填就表示不限制最小次数。然后是手机,手机为11位,且由1开头,且都为数字,那么就很简单了:^1\d{10}$。如何把他们拼起来?字符 | 可以办到,下面是代码。
public static void Lesson2(){
Console.WriteLine("是否为合法的电话号码,电话号码的规则分两部分:");
Console.WriteLine("固话为7-8位,手机为11位,且由1开头,且都为数字。");
string landPhoneRule = @"^\d{7,8}$";
string handPhoneRule = @"^1\d{10}$";
//规则合并
string rule = string.Format("{0}|{1}", landPhoneRule, handPhoneRule);
string[] enters ={
"1234567", //7位数字,合法
"12345678", //8位数字,合法
"13888888888", //11位以1开头的数字,合法
"23888888888", //11位以2开头的数字,非法
"0123456789", //10位数字,非法
"1388888888a",//带有字符,非法
"10111111111"//11数字,合法
};
foreach (var enter in enters){
Console.WriteLine(Regex.Match(enter, rule));
}
Console.WriteLine("现改变手机的规则,改为:手机要以数字1开头,且第二位和第三位不能有数字0,其他不变");
handPhoneRule = @"^1[1-9]{2}\d{8}$";
rule = string.Format("{0}|{1}", landPhoneRule, handPhoneRule);
foreach (var enter in enters){
Console.WriteLine(Regex.Match(enter, rule));
}
}
我们看到10111111111也能匹配,显然这不是个手机号,因此在后面我更改了规则,添加了第二位和第三位不是0的限制。这时 \d 不满足条件了,查看手册,发现 [] 字符,可以在里面添加候选字符,例如 [123] 指匹配123,也可以用 - 来添加范围,如 [0-9] 和 \d 是一样的。那么更改后的手机号码匹配的规则就变成了 ^1[1-9]{2}\d{8}$ 。下面是第三课。
- Lesson3 判断IP是否合法
这一题,我将IP的判断稍微简化了些,规则是必须是 ***.***.***.*** ,其中每项的字符数最少为1位,最多为3位,且 255>=*** >= 0, 第一项不能为0\00\000。这一题的判断要复杂的多,您也能看到正则表达式的一个”短板“,那就是不能获取字符的意义,后面我会解释这一点。看条件,基本的语法我前面都介绍了,该规则可以拆分为两部分,第一项和后三项。第一项,255>=*** >= 0, 且不能为0\00\000。我在思考这道题的时候,先按照正向的思路想,即”描述什么样的满足条件“,情况非常多,01,001,011,1-249,250-255,一共这么多种情况,正则表达式是没有办法”忽略“的,如果是数字, if (001 == 1)是成立的,但是正则表达式办不到,你只能 (0[2]1)|1 来描述1和001都满足条件,这就是我前面说的,无法获取字符本身的意义,你只能每一位的描述一个字符串,第一位是什么,第二位不是什么,而无法通过意义来形容字符串。我通过”正向”的列举每种情况,表达式是这样的:(0{2}[1-9]|0[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|0[1-9]|[1-9]\d|[1-9]) 。其意义大概就是把01,001,011,1-249,250-255的全部情况都列举了出来,这么做没什么“错”,但是太长了,有没有什么简便做法?“正向”思考不行,那么我们来“反向”试一下:第一项是1-3个数字,不能全是0,不能大于255,也就是不能是 ^0{1,3},2[6-9]\d,25[6-9],[3-9]\d{2},只要不是前面的条件,就可以。如何形容不满足的条件? [^]可以,但是只能指明一个字符。可以看到手册的下面有几个?<!这样的字符,他们表示“附近是否满足条件”,举个例子:?! 加一起表示正向否定的预查找, Windows(?!95|98|NT|2000)
”能匹配“Windows3.1
”中的“Windows
”,但不能匹配“Windows2000
”中的“Windows
”,还有其他的。这里我用?<! ,表示反向否定查找,即(?<!123)456表示能匹配23456,但是不能匹配前面带123的456,如123456就不满足条件。这个正向和反向就是前面和后面,肯定就是匹配,否定就是排除。我的第一项的表达式是:^(\d{1,3})(?<!^0{1,3}|2[6-9]\d|25[6-9]|[3-9]\d{2}),第二项的前面如果是上述几种情况,则第二项永远不会匹配。接下来是后面三项,后面的三项都是 .***,255>=***>=0,语句是(.[01]?\d?\d|2[0-4]\d|25[0-5]){3}$,加起来就是 ^(\d{1,3})(?<!^(0{1,3}|2[6-9]\d|25[6-9]|[3-9]\d{2}))(.[01]?\d?\d|2[0-4]\d|25[0-5]){3}$ 。代码如下:
public static void Lesson3(){
Console.WriteLine("题目是判断一个IP是否合法,规则如下:");
Console.WriteLine("格式必须是***.***.***.***");
Console.WriteLine("其中第一组数字必须大于1, 每组数字都要小于等于255");
//复杂版 string rule = @"^(\d{1,3})(?<!^(0{1,3}|2[6-9]\d|25[6-9]|[3-9]\d{2}))(.[01]?\d?\d|2[0-4]\d|25[0-5]){3}$";
string[] enters =
{
"255.255.255.255", //合法 "21.1.1.1", //合法 "256.0.0.0", //非法
"300.2.2.250", //非法 "10.1.1.99", //合法 "00.1.1.009",//非法
"100.1.1.1"//合法 };
foreach (var enter in enters){
Console.WriteLine(Regex.Match(enter, rule).Convert2String(enter));
}
}
这里要吐槽下,如果能转成数字,判断IP是否合法,只要把数字截出来,判断 >=0&& <=255就可以了,这里也可能有更简便的做法,笔者一时没想出来,欢迎有好办法的同学留言给我。
- Lesson4 函数替换
这个题目是我学习正则表达式的起源。当时我在重构代码,发现有一个方法写的多余我们管这个多余的方法叫MA,应该换成MB(另一个方法),MA接受2个变量,MB接受一个变量。VS提供了自动更换函数名字的功能,但是对于函数变量的变化就无能为力了。当时全工程有接近50个MA的调用,都要替换成MB,如果手动一下一下的删除多余的参数,那实在是太恶心了。VS查找界面提供了替换功能,我看到搜索选项里面有正则表达式一项,是否能够使用正则表达式来自动替换?于是就开始研究起正则表达式,是1天还是2天,磕磕绊绊,研究出了表达式,然后一试,确实好使!怕匹配错,就点击替换,一个一个的换,中间多次调整表达式,最终在没有手动更改的情况下,全部替换成功,当时真的很高兴,感觉正则表达式很神奇。因此我就把这个情景转换为第四课的内容,前面讲的都是匹配,这一课就来看看如何进行替换。下面是题目的要求,上面的一组Console.WriteLine是原语句,下面的4个Console.WriteLine是替换后的样子。就是用MB替换MA,并保留MA的第一个参数给MB,舍弃第二个参数。每条语句都毫无意义,只是用来替换。
//下面的语句没有任何的实际意义,只是模拟 想要替换的语句的具体使用。Console.WriteLine(MA("a", "b")),
Console.WriteLine("a" + MA("a", GetType().ToString()));
Console.WriteLine(MA("a", GetType().ToString()));
Console.WriteLine("a" + MA("a", "a".Substring(1)) + "b");
//替换成如下的样子Console.WriteLine(MB("a"));
Console.WriteLine("a" + MB("a"));
Console.WriteLine(MB("a"));
Console.WriteLine("a" + MB("a") + "b");
//这两个方法没有任何意义,只是用MA模拟原函数,MB模拟想要被替换的函数public string MA(string a, string b) { return null; }
public string MB(string a) { return null; }
先来匹配,分析一下要替换的4条语句:开始的部分都是函数名MA,内容是()内的部分,只要找到适当的‘(’和‘)’,就可以了。但事实是无法确定到底哪个‘)’才是合适的,能看到后的语句后面有一个‘)’,有的是两个,有的是三个。这里括号要精确匹配,不能匹配多了,不然语句就错误了。一个‘)’的情况是带个",其他的至少有2个‘)’,因此我们可以分两种情况,第一种是"),第二种是多个括号的前两个。这里有一个问题,如何只匹配前两个?在正则表达式中,有两种匹配方式,一种是贪心,一种是非贪心。贪心的意思是能匹配几个就匹配几个,比如a+,在匹配aaaaaab的时候,会匹配全部的a字母,结果是aaaaaa。而如果加入a+?,?的本来意思是0个或1个,但是在这里,表示最多匹配1个,结果就是a。还有一个问题就是,正则表达式中,()是有特殊意义的,如果放弃其特殊意义,只是想匹配括号,就要用转义副\,这个用过ASC||码的都应该能明白。则匹配MA函数的表达式为:MA\((.+),\s?.+?(\)\))|""\) 。注意,c#中双引号要这样 @"""",前面用@,然后用两个""表示一个双引号。读别人的正则表达式会有些费劲,那么我的建议是你可以先不看我的结果,只要明白题意,一边查看手册,一边自己练习一点一点的试,很快就能做出来,这时 你就明白大概了,再来看拿自己的表达式与别人的对比,很有可能你做的比我简略!
可以看到在\s?之后,我用.+?来尽量少的匹配字符,直到遇到))或者")就停止匹配,如果只用.+,就会过多的匹配括号。 光有正则表达式还不够,还要能替换。替换的难点在如何保留第一个变量。正则表达式提供了提取括号里匹配到则值得机制,如(\w)\1(\w)\2,能够匹配aabb,第一个\w匹配到了a,由于在括号中,因此被记录了下来,可通过\1来获得第一个括号中的内容,以此类推。在替换时,C#(其他语言不清楚),可利用$1来获得第一个括号中匹配到的内容。则替换的语句是MB($1)。注意,这里替换的语句不需要转义,因为替换不需要匹配,只要将字符原封不动替换就可以了。下面是代码:
public static void Lesson4(){
Console.WriteLine("第四课,替换函数。");
string rule = @"MA\((.+?),\s?.+?(\)\)|""\))";
string[] enters =
{
@"Console.WriteLine(MA(""a"", ""b""))",
@"Console.WriteLine(""a"" + MA(""a"", GetType().ToString()));",
@"Console.WriteLine(MA(""a"", GetType().ToString()));",
@"Console.WriteLine(""a"" + MA(""a"", ""a"".Substring(1)) + ""b"");"
};
string replacement = @"MB($1)";
foreach (var enter in enters)
{
Console.WriteLine(Regex.Match(enter, rule).Convert2String(enter));
Console.WriteLine("替换后由{0}变为{1}", enter, Regex.Replace(enter, rule, replacement));
}
}
正则表达式看起来很难,但如果您把这4课都做一遍,就已经基本掌握了正则表达式了,日常的应用是没有问题的。语法有些难记,我的建议是先不管语法,要用就随时翻手册。欢迎各位同学在评论区与我互动。