正则表达式提供了功能强大、灵活而又高效的方法来处理文本。正则表达式的全面模式匹配表示法使您可以快速分析大量文本以找到特定的字符模式;提取、编辑、替换或删除文本子字符串;或将提取的字符串添加到集合以生成报告。对于处理字符串(例如 HTML 处理、日志文件分析和 HTTP 标头分析)的许多应用程序而言,正则表达式是不可缺少的工具。正则表达式是一个非常有用的技术,有人曾称之为能让程序员不至于丢掉饭碗的十大技术之一,可见它的重要性。
熟悉 DOS 或者命令行的朋友或许已经用过类似的功能,比如我们要查找 D 盘下所有的低于 Word2007 版本的 Word 文件(因为低于 Word2007 版本的 Word 文件的文件后缀是 .doc ,而 Word2007 版本的 Word 文件的文件后缀是 .docx ),我们可以在命令行下执行这个命名:
dir D:/*doc
当然如果想查找 D 盘下任意级子目录下的所有此类文件,就应该执行 dir /s D:/*doc 了。
注意正则表达式并不是在 C# 中独有的东东,实际上在其它语言中早就实现了,比如 Perl (可能很多人没有听说过这个编程语言,十年前大学期间我曾经学过一点皮毛),其它的编程语言 Java 、 PHP 及 JavaScript 等也支持正则表达式,正则表达式差不多像 SQL 语言一样成为标准了,同样和 SQL 类似,在不同的数据库厂商那里对 SQL 标准支持的程度并不完全一样,正则表达式也是如此,大部分内的正则表达式可以跨语言使用,但是在各语言中也会有细微的区别,这一点是需要我们注意的。
正则表达式元字符
正则表达式语言由两种基本字符类型组成:原义(正常)文本字符和元字符。元字符使正则表达式具有处理能力。元字符既可以是放在 [] 中的任意单个字符(如 [a] 表示匹配单个小写字符 a ),也可以是字符序列(如 [a-d] 表示匹配 a 、 b 、 c 、 d 之间的任意一个字符,而 /w 表示任意英文字母和数字及下划线),下面是一些常见的元字符:
元字符 |
说明 |
. |
匹配除 /n 以外的任何字符(注意元字符是小数点)。 |
[abcde] |
匹配 abcde 之中的任意一个字符 |
[a-h] |
匹配 a 到 h 之间的任意一个字符 |
[^fgh] |
不与 fgh 之中的任意一个字符匹配 |
/w |
匹配大小写英文字符及数字 0 到 9 之间的任意一个及下划线,相当于 [a-zA-Z0-9_] |
/W |
不匹配大小写英文字符及数字 0 到 9 之间的任意一个,相当于 [^a-zA-Z0-9_] |
/s |
匹配任何空白字符,相当于 [ /f/n/r/t/v] |
/S |
匹配任何非空白字符,相当于 [^/s] |
/d |
匹配任何 0 到 9 之间的单个数字,相当于 [0-9] |
/D |
不匹配任何 0 到 9 之间的单个数字,相当于 [^0-9] |
[/u4e00-/u9fa5] |
匹配任意单个汉字(这里用的是 Unicode 编码表示汉字的 ) |
正则表达式限定符
上面的元字符都是针对单个字符匹配的,要想同时匹配多个字符的话,还需要借助限定符。下面是一些常见的限定符 ( 下表中 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 ”作为示例字符串,如果正则表达式是“ /b/d{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 = ; i < matchCollection.Count; i++)
{
Match match = matchCollection[i];
Console.WriteLine("Match[{0}]========================", i);
for (int j = ; 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 = ; 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("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");
说明,其实关于正则表达式还有比较多的内容可以讲,比如反向引用、匹配顺序及几种匹配模式的区别和联系等,不过这些在日常开发中使用不是太多(如果做文本分析处理还是会用到的),所以暂时不会继续讲了。尽管本系列四篇文章篇幅都不是太长(本人不敢太熬夜了,因为每天
5
点多就要起床),不过通过这些基础的学习仍是可以掌握正则表达式的精华之处的,至于在开发中怎么样去用,就要靠我们自己灵活去结合实际情况用了。我个人经验是如果是用于验证是否满足要求,那么写正则表达式时写得严格一点,如果是从规范格式的文本中提取数据,则可以写得宽松一点,比如验证时间,则必须写成
(?<time>(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2})
这种形式,这样别人输入
26:99:99
就不能通过验证,但是如果是像从上面提到的
IIS
日志中提取时间,用
(?<time>/d{2}(:/d{2}){2})
这种方式也是可以,当然如果写比较严格的验证比较麻烦时也可以写比较宽松的格式,然后借助其它手段来验证,在网上有一个验证日期的正则表达式,编写者充分考虑到各个月份天数的不同、甚至平年和闰年
2
月份天数的不同的情况写了一个相当复杂的正则表达式来验证,个人觉得可以结合将文本值转换成日期的方式来共同验证,这样更好理解和接受些。
C#正则表达式编程(四):正则表达式的更多相关文章
-
Python笔记_第四篇_高阶编程_正则表达式_3.正则表达式深入
1. re.split 正则的字符串切割 str1 = "Thomas is a good man" print(re.split(r" +",str1)) # ...
-
C#正则表达式编程(一):C#中有关正则的类
正则表达式是一门灵活性非常强的语言,匹配同样的字符串可能在不同的开发人员那里会得到不同的结果,在平常的时候也是用的时候看看相关资料,不用的时候就丢在脑后了,尽管在处理大部分情况下都能迅速处理,但是处理 ...
-
Linux学习——shell编程之正则表达式和字符处理命令
shell编程之正则表达式 一 正则表达式 1 什么是正则表达式 正则表达式用于描述字符排列和匹配模式的一种语法规则.它主要用于字符串的模式分隔.匹配.查找及替换操作. 2 shell编程之正则表达式 ...
-
spring cloud: zuul(四): 正则表达式匹配其他微服务(给其他微服务加版本号)
spring cloud: zuul(四): 正则表达式匹配其他微服务(给其他微服务加版本号) 比如我原来有,spring-boot-user微服务,后台进行迭代更新,另外其了一个微服务: sprin ...
-
05 shell编程之正则表达式
正则表达式&&文本处理利器 学习目标: l 掌握正则表达式的运用 l 掌握sed.awk文本处理工具的使用 目录结构: 正则表达式 正则表达式概述 l 正则表达式:使用单个字 ...
-
js备战春招の四の正则表达式详解
正则表达式语法规则:/正则表达式主体/修饰符(可选)什么是正则表达式:正则表达式是用于匹配字符串中字符组合的模式.在 JavaScript中,正则表达式也是对象.这些模式被用于 RegExp 的 ex ...
-
【学习笔记】第二章 python安全编程基础---正则表达式
一.python正则表达式 定义:正则表达式是一个特殊的字符序列,它能帮助你方便的检查一个字符串是否与某种模式相匹配: 1.1RE模块:是python语言拥有全部的正则表达式功能的一个正则模块: 常见 ...
-
Linux通配符与基础正则表达式、扩展正则表达式
在Linux命令行操作或者SHELL编程中总是容易混淆一些特殊字符的使用,比如元字符‘*’号,作为通配符匹配文件名时表示0个到无穷多个任意字符.而作为正则表达式匹配字符串时,表示重复0个到无穷多个的前 ...
-
shell 编程四剑客简介 find sed grep awk(微信公众号摘抄)
一,Shell编程四剑客之Find 通过如上基础语法的学习,读者对Shell编程有了更近一步的理解,Shell编程不再是简单命令的堆积,而是演变成了各种特殊的语句.各种语法.编程工具.各种命令的集合. ...
随机推荐
-
BZOJ2815: [ZJOI2012]灾难
传送门 学LCA的时候根本没意识到LCA可以有这么多玩法. 这玩意据说是个高级数据结构(支配树)的弱化版,蒟蒻没学过呀.所以出题人提出一个概念叫灾难树. 我理解的灾难树的意思实际上是属于DAG的一个子 ...
-
CSS对浏览器的兼容性(IE和Firefox)技巧整理
CSS对浏览器的兼容性有时让人很头疼,或许当你了解当中的技巧跟原理,就会觉得也不是难事,从网上收集了IE7,6与Fireofx的兼容性处理技巧并整理了一下.对于web2.0的过度,请尽量用xhtml格 ...
-
基于jquery的表单校验插件 - formvalidator使用体验
下载地址:http://www.formvalidator.net/ 基本样例 <form action="/registration" method="POST& ...
-
vs 2013调试的时候重启的解决方案
今天在用vs 2013 调试程序的时候,vs 总是莫名其妙的关闭,停止运行,泪蹦了..... 是什么原因呢? 以前的时候可是好好的啊,经过认真的思索,最近装过和vs 2013 相关的程序也只有 ref ...
-
R12.2常用手册
>>Related Information Sources这本书包含在Oracle电子商务套件文档库中.如果该指南将您引用到其他Oracle电子商务套件文档中,只使用这些指南的最新版本12 ...
-
12 Zabbix Item类型之Zabbix JMX类型
点击返回:自学Zabbix之路 12 Zabbix Item类型之Zabbix JMX类型 JMX 全称是Java Management Extensions,即Java管理扩展.Java程序会开放一 ...
-
ss源码学习--从协议建立到完成一次代理请求
上一次介绍了ss源码中各个事件处理函数完成的工作,这次具体分析一下协议的建立以及请求数据的传输过程. 因为ss的local和server共用一个类以及一系列的事件处理函数,所以看起来稍显复杂.下面来将 ...
-
dj forms表单组件
手动的一个个去校验前端传过来的字段数据,是很麻烦的,利用django 的forms组件,对需要校验的字段定义好,能够大大提高效率. 校验字段功能 from django.db import model ...
-
以Java的视角来聊聊BIO、NIO与AIO的区别?
转: 以Java的视角来聊聊BIO.NIO与AIO的区别? 飞丫玲丫 17-07-2623:10 题目:说一下BIO/AIO/NIO 有什么区别?及异步模式的用途和意义? BIO(Blocking I ...
-
终端(terminal)、tty、shell、控制台(console)、bash之间的区别与联系
1.终端(terminal) 终端(termimal)= tty(Teletypewriter, 电传打印机),作用是提供一个命令的输入输出环境,在linux下使用组合键ctrl+alt+T打开的就是 ...