一提到正则表达式,相信好多人都不会陌生,并且很多时候,我们都用过,比如说验证邮箱或者是手机号码的正确性等等,在.NET中,提供了强大的正则表达式辅助类,最主要的还是要数Regex类,利用这个类,可以非常方便的来操作正则表达式的匹配工作:
代码如下 | 复制代码 |
string matchText = "this|is|test"; Regex reg = new Regex(@"[a-z]+|"); MatchCollection mc = reg.Matches(matchText); foreach (Match myValue in mc) { MessageBox.Show(myValue.Value); } |
上面就是一个非常简单的例子,用来匹配英文字符以及竖线,这里匹配出来的结果就是this|和is|。需要注意的是,这里我们需要引入命名空间:
using System.Text.RegularExpressions;
下面具体来说明正则表达式。
首先,对于正则表达式,我们需要知道三个命令,用英文字母表示,就是BCD,怎么说呢?
B指三种括符类型:中括号[],它是用来匹配你需要匹配的字串;大括号{},它是用来指定匹配的长度;小括号(),它是用来分组。
C 指脱字符,也即上尖角符号(^),它是用来指定匹配的开始。
D指美元符号,也即$,它是用来指定匹配的结束。
下面以一个例子来说明:
首先我这里有一堆邮件地址的集合:
"用户一"<491204829@qq.com>,
"用户二"<12340352@qq.com>,
"用户三"<962390304@qq.com>,
"用户四"<xylw2y2011@163.com>,
"用户五"<443225735@qq.com>,
"用户六"519733331@qq.com
然后我们只需要提取其后面的实际邮件地址,那么这个该如何来做呢?
首先我们观察,后面的邮件地址都是数字或者是字母的集合,然后带上@符号,然后都是以数字或者字母结合,最后都是以.com结束,知道了大致的规则后,我们来进行匹配。
首先是在<内的数字或者字母的集合,[a-zA-Z0-9]指匹配a到z或者是A到Z或者是0-9的字串,长度一般都是10位左右,那么就匹配1到10位的:{1,10},然后就是@符号后面,首先在点号之前的也是字母或者数字的组合[a-zA-Z0-9],同样我们限定长度为10位之间{1,10},然后是点号后面的,我们可以利用(com|org)进行匹配,这样,匹配出来的结果为:
代码如下 | 复制代码 |
<[a-zA-Z0-9]{1,10}@[a-zA-Z0-9]{1,10}.(com|org)> |
下面通过代码来说明:
代码如下 | 复制代码 |
string myMailInfo = this.txtUserMail.Text; Regex reg = new Regex(@"<[a-zA-Z0-9]{1,10}@[a-zA-Z0-9]{1,10}.(com|org)>"); //Regex reg = new Regex(@"<S{1,20}@S{6}>"); MatchCollection mc = reg.Matches(myMailInfo); this.txtUserMail.Text = string.Empty; for (int i = 0; i < mc.Count; i++) { this.txtUserMail.AppendText(mc[i].Value+","); } |
这样就很容易的将结果提取出来,结果如下:
<491204829@qq.com>,
<12340352@qq.com>,
<962390304@qq.com>,
<xylw2y2011@163.com>,
<443225735@qq.com>,
<519733331@qq.com>,
所以这个需要找出一定的规律,然后进行的话,将会变得比较容易一些。下面是匹配的Mapping关系:
匹配字母或者数字
[a-zA-Z0-9]
匹配长度在1到10之间的字母或数字组合
[a-zA-Z0-9]{1,10}
匹配固定字符串com或者是org
(com|org)
当然,其实如果说不匹配任何字符或者数字或者其他字串,该怎么弄呢?其实,可以使用^来决定,如果不想以字母开头,可以利用^w来表示,不想以数字开头,可以利用^d来表示,不想以任何空白字符开头,可以利用^s来表示,这个其实还有更简单的写法,也即:^w 对应于 W ; ^d对应于D;^s对应于S。
下面是一些常见的元字符:
元字符
说明
.
匹配除 n 以外的任何字符(注意元字符是小数点)。
[abcde]
匹配abcde之中的任意一个字符
[a-h]
匹配a到h之间的任意一个字符
[^fgh]
不与fgh之中的任意一个字符匹配
w
匹配大小写英文字符及数字0到9之间的任意一个及下划线,相当于[a-zA-Z0-9_]
W
不匹配大小写英文字符及数字0到9之间的任意一个,相当于[^a-zA-Z0-9_]
s
匹配任何空白字符,相当于[ fnrtv]
S
匹配任何非空白字符,相当于[^s]
d
匹配任何0到9之间的单个数字,相当于[0-9]
D
不匹配任何0到9之间的单个数字,相当于[^0-9]
[u4e00-u9fa5]
匹配任意单个汉字
下面一部分内容来自周公的博客,个人认为讲解的非常好:
上面的元字符都是针对单个字符匹配的,要想同时匹配多个字符的话,还需要借助限定符。下面是一些常见的限定符(下表中n和m都是表示整数,并且0<n<m):
限定符 说明
-----------------------------------------------
* 匹配0到多个元字符,相当于{0,}
-----------------------------------------------
? 匹配0到1个元字符,相当于{0,1}
-----------------------------------------------
{n} 匹配n个元字符
-----------------------------------------------
{n,} 匹配至少n个元字符
-----------------------------------------------
{n,m} 匹配n到m个元字符
-----------------------------------------------
+ 匹配至少1个元字符,相当于{1,}
-----------------------------------------------
b 匹配单词边界
-----------------------------------------------
^ 字符串必须以指定的字符开始
-----------------------------------------------
$ 字符串必须以指定的字符结束
-----------------------------------------------
说明:
(1)由于在正则表达式中“”、“?”、“*”、“^”、“$”、“+”、“(”、“)”、“|”、“{”、“[”等字符已经具有一定特殊意义,如果需要用它们的原始意义,则应该对它进行转义,例如希望在字符串中至少有一个“”,那么正则表达式应该这么写:\+。
(2)可以将多个元字符或者原义文本字符用括号括起来形成一个分组,比如^(13)[4-9]d{8}$表示任意以13开头的移动手机号码。
(3)另外对于中文字符的匹配是采用其对应的Unicode编码来匹配的,对于单个Unicode字符,如u4e00表示汉字“一”, u9fa5表示汉字“?”,在Unicode编码中这分别是所能表示的汉字的第一个和最后一个的Unicode编码,在Unicode编码中能表示20901个汉字。
(4)关于b的用法,它代表单词的开始或者结尾,以字符串“123a 345b 456 789d”作为示例字符串,如果正则表达式是“bd{3}b”,则仅能匹配456。
(5)可以使用“|”来表示或的关系,例如[z|j|q]表示匹配z、j、q之中的任意一个字母。
正则表达式分组(本部分及以下来源自周公博客:)
将正则表达式的一部分用()括起来就可以形成一个分组,也叫一个子匹配或者一个捕获组。例如对于“08:14:27”这样格式的时间,我们可以写如下的正则表达式:
((0[1-9])|(1[0-9])|(2[0-3])(:[0-5][1-9]){2}
如果以这个作为表达式,它将从下面的一段IIS访问日志中提取出访问时间(当然分析IIS日志最好的工具是Log Parser这个微软提供的工具):
00:41:23 GET /admin_save.asp 202.108.212.39 404 1468 176
01:04:36 GET /userbuding.asp 202.108.212.39 404 1468 176
10:00:59 GET /upfile_flash.asp 202.108.212.39 404 1468 178
12:59:00 GET /cp.php 202.108.212.39 404 1468 168
19:23:04 GET /sqldata.php 202.108.212.39 404 1468 173
23:00:00 GET /Evil-Skwiz.htm 202.108.212.39 404 1468 176
23:59:59 GET /bil.html 202.108.212.39 404 1468 170
如果我们想对上面的IIS日志进行分析,提取每条日志中的访问时间、访问页面、客户端IP及服务器端响应代码(对应C#中的HttpStatusCode),我们可以按照分组的方式来获取。
代码如下:
private String text= @"00:41:23 GET /admin_save.asp 202.108.212.39 404 1468 176
01:04:36 GET /userbuding.asp 202.108.212.39 404 1468 176
10:00:59 GET /upfile_flash.asp 202.108.212.39 404 1468 178
12:59:00 GET /cp.php 202.108.212.39 404 1468 168
19:23:04 GET /sqldata.php 202.108.212.39 404 1468 173
23:00:00 GET /Evil-Skwiz.htm 202.108.212.39 404 1468 176
23:59:59 GET /bil.html 202.108.212.39 404 1468 170";
/// <summary>
/// 分析IIS日志,提取客户端访问的时间、URL、IP地址及服务器响应代码
/// </summary>
public void AnalyzeIISLog()
{
//提取访问时间、URL、IP地址及服务器响应代码的正则表达式
//大家可以看到关于提取时间部分的子表达式比较复杂,因为做了比较严格的时间匹配限制
//注意为了简化起见,没有对客户端IP格式进行严格验证,因为IIS访问日志中也不会出现不符合要求的IP地址
代码如下 | 复制代码 |
Regex regex = new Regex(@ "((0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2})s(GET)s([^s]+)s(d{1,3}(.d{1,3}){3})s(d{3})", RegexOptions.None); MatchCollection matchCollection = regex.Matches(text); for (int i = 0; i < matchCollection.Count; i++) { Match match = matchCollection[i]; Console.WriteLine( "Match[{0}]========================", i); for (int j = 0; j < match.Groups.Count; j++) { Console.WriteLine( "Groups[{0}]={1}", j, match.Groups[j].Value); } } } |
这段代码的输出结果如下:
Match[0]========================
Groups[0]=00:41:23 GET /admin_save.asp 202.108.212.39 404
Groups[1]=00:41:23
Groups[2]=00
Groups[3]=:23
Groups[4]=GET
Groups[5]=/admin_save.asp
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
Match[1]========================
Groups[0]=01:04:36 GET /userbuding.asp 202.108.212.39 404
Groups[1]=01:04:36
Groups[2]=01
Groups[3]=:36
Groups[4]=GET
Groups[5]=/userbuding.asp
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
Match[2]========================
Groups[0]=10:00:59 GET /upfile_flash.asp 202.108.212.39 404
Groups[1]=10:00:59
Groups[2]=10
Groups[3]=:59
Groups[4]=GET
Groups[5]=/upfile_flash.asp
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
Match[3]========================
Groups[0]=12:59:00 GET /cp.php 202.108.212.39 404
Groups[1]=12:59:00
Groups[2]=12
Groups[3]=:00
Groups[4]=GET
Groups[5]=/cp.php
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
Match[4]========================
Groups[0]=19:23:04 GET /sqldata.php 202.108.212.39 404
Groups[1]=19:23:04
Groups[2]=19
Groups[3]=:04
Groups[4]=GET
Groups[5]=/sqldata.php
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
Match[5]========================
Groups[0]=23:00:00 GET /Evil-Skwiz.htm 202.108.212.39 404
Groups[1]=23:00:00
Groups[2]=23
Groups[3]=:00
Groups[4]=GET
Groups[5]=/Evil-Skwiz.htm
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
Match[6]========================
Groups[0]=23:59:59 GET /bil.html 202.108.212.39 404
Groups[1]=23:59:59
Groups[2]=23
Groups[3]=:59
Groups[4]=GET
Groups[5]=/bil.html
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
从上面的输出结果中我们可以看出在每一个匹配结果中,第2个分组就是客户端访问时间(因为索引是从0开始的,所以索引顺序为1,以下同理),第6个分组是访问的URL(索引顺序为6),第7个分组是客户端IP(索引顺序为6),第9个分组是服务器端响应代码(索引顺序为9)。如果我们要提取这些元素,可以直接按照索引来访问这些值就可以了,这样比我们不采用正则表达式要方便多了。
命名捕获组
上面的方法尽管方便,但也有一些不便之处:假如需要提取更多的信息,对捕获组进行了增减,就会导致捕获组索引对应的值发生变化,我们就需要重新修改代码,这也算是一种硬编码吧。有没有比较好的办法呢?答案是有的,那就是采用命名捕获组。
就像我们使用DataReader访问数据库或者访问DataTable中的数据一样,可以使用索引的方式(索引同样也是从0开始),不过如果变化了select语句中的字段数或者字段顺序,按照这种方式获取数据就需要重新变动,为了适应这种变化,同样也允许使用字段名作为索引来访问数据,只要数据源中存在这个字段而不管顺序如何都会取到正确的值。在正则表达式中命名捕获组也可以起到同样的作用。
普通捕获组表示方式:(正则表达式),如(d{8,11});
命名捕获组表示方式:(?<捕获组命名>正则表达式),如(?<phone>d{8,11})
对于普通捕获组只能采用索引的方式获取它对应的值,但对于命名捕获组,还可以采用按名称的方式访问,例如(?<phone>d{8,11}),在代码中就可以按照match.Groups["phone"]的方式访问,这样代码更直观,编码也更灵活,针对刚才的对IIS日志的分析,我们采用命名捕获组的代码如下:
private String text= @"00:41:23 GET /admin_save.asp 202.108.212.39 404 1468 176
01:04:36 GET /userbuding.asp 202.108.212.39 404 1468 176
10:00:59 GET /upfile_flash.asp 202.108.212.39 404 1468 178
12:59:00 GET /cp.php 202.108.212.39 404 1468 168
19:23:04 GET /sqldata.php 202.108.212.39 404 1468 173
23:00:00 GET /Evil-Skwiz.htm 202.108.212.39 404 1468 176
23:59:59 GET /bil.html 202.108.212.39 404 1468 170";
/// <summary>
/// 采用命名捕获组提取IIS日志里的相关信息
/// </summary>
代码如下 | 复制代码 |
public void AnalyzeIISLog2() { Regex regex = new Regex(@ "(?<time>(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2})s(GET)s(?<url>[^s]+)s(?<ip>d{1,3}(.d{1,3}){3})s(?<httpCode>d{3})", RegexOptions.None); MatchCollection matchCollection = regex.Matches(text); for (int i = 0; i < matchCollection.Count; i++) { Match match = matchCollection[i]; Console.WriteLine( "Match[{0}]========================", i); Console.WriteLine( "time:{0}", match.Groups["time"]); Console.WriteLine( "url:{0}", match.Groups["url"]); Console.WriteLine( "ip:{0}", match.Groups["ip"]); Console.WriteLine( "httpCode:{0}", match.Groups["httpCode"]); } } |
这段代码的执行效果如下:
Match[0]========================
time:00:41:23
url:/admin_save.asp
ip:202.108.212.39
httpCode:404
Match[1]========================
time:01:04:36
url:/userbuding.asp
ip:202.108.212.39
httpCode:404
Match[2]========================
time:10:00:59
url:/upfile_flash.asp
ip:202.108.212.39
httpCode:404
Match[3]========================
time:12:59:00
url:/cp.php
ip:202.108.212.39
httpCode:404
Match[4]========================
time:19:23:04
url:/sqldata.php
ip:202.108.212.39
httpCode:404
Match[5]========================
time:23:00:00
url:/Evil-Skwiz.htm
ip:202.108.212.39
httpCode:404
Match[6]========================
time:23:59:59
url:/bil.html
ip:202.108.212.39
httpCode:404
采用命名捕获组之后使访问捕获组的值更直观了,而且只要命名捕获组的值不发生变化,其它的变化都不影响原来的代码。
非捕获组
如果经常看别人有关正则表达式的源代码,可能会看到形如(?: 子表达式)这样的表达式,这就是非捕获组,对于捕获组我们可以理解,就是在后面的代码中可以通过索引或者名称(如果是命名捕获组)的方式来访问匹配的值,因为在匹配过程中会将对应的值保存到内存中,如果我们在后面不需要访问匹配的值那么就可以告诉程序不用在内存中保存匹配的值,以便提高效率减少内存消耗,这种情况下就可以使用非捕获组,例如在刚刚分析IIS日志的时候我们对客户端提交请求的方式并不在乎,在这里就可以使用非捕获组,如下:
代码如下 | 复制代码 |
Regex regex = new Regex(@"(?<time>(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2})s(?:GET)s(?<url>[^s]+)s(?<ip>d{1,3}(.d{1,3}){3})s(?<httpCode>d{3})"; |
零宽度断言
关于零宽度断言有多种叫法,也有叫环视、也有叫预搜索的,我这里采用的是MSDN中的叫法,关于零宽度断言有以下几种:
(?= 子表达式):零宽度正预测先行断言。仅当子表达式在此位置的右侧匹配时才继续匹配。例如,19(?=99) 与跟在99前面的19实例匹配。
(?! 子表达式):零宽度负预测先行断言。仅当子表达式不在此位置的右侧匹配时才继续匹配。例如,(?!99)与不以99结尾的单词匹配,所以不与1999匹配。
(?<= 子表达式):零宽度正回顾后发断言。仅当子表达式在此位置的左侧匹配时才继续匹配。例如,(?<=19)99 与跟在 19 后面的 99 的实例匹配。此构造不会回溯。
(?<! 子表达式):零宽度负回顾后发断言。仅当子表达式不在此位置的左侧匹配时才继续匹配。例如(?<=19)与不以19开头的单词匹配,所以不与1999匹配。
正则表达式选项
在使用正则表达式时除了使用RegexOptions这个枚举给正则表达式赋予一些额外的选项之外,还可以在在表达式中使用这些选项,如:
代码如下 | 复制代码 |
Regex regex = new Regex("(?i)def"); Regex regex = new Regex("(?i)def"); |
它与下面一句是等效的:
代码如下 | 复制代码 |
Regex regex = new Regex("def", RegexOptions.IgnoreCase); |
Regex regex = new Regex("def", RegexOptions.IgnoreCase);
采用(?i)这种形式的称之为内联模式,顾名思义就是在正则表达式中已经体现了正则表达式选项,这些内联字符与RegexOptions的对应如下:
IgnoreCase:内联字符为i,指定不区分大小写的匹配。
Multiline:内联字符为m,指定多行模式。更改 ^ 和 $ 的含义,以使它们分别与任何行的开头和结尾匹配,而不只是与整个字符串的开头和结尾匹配。
ExplicitCapture:内联字符为n,指定唯一有效的捕获是显式命名或编号的 (?<name>…) 形式的组。这允许圆括号充当非捕获组,从而避免了由 (?:…) 导致的语法上的笨拙。
Singleline:内联字符为s,指定单行模式。更改句点字符 (.) 的含义,以使它与每个字符(而不是除 n 之外的所有字符)匹配。
IgnorePatternWhitespace:内联字符为x,指定从模式中排除非转义空白并启用数字符号 (#) 后面的注释。(有关转义空白字符的列表,请参见字符转义。) 请注意,空白永远不会从字符类中消除。
举例说明:
代码如下 | 复制代码 |
RegexOptions option=RegexOptions.IgnoreCase|RegexOptions.Singleline; Regex regex = new Regex("def", option); |
用内联的形式表示为:
代码如下 | 复制代码 |
Regex regex = new Regex("(?is)def"); |