C#正则表达式编程(四):正则表达式

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:
正则表达式提供了功能强大、灵活而又高效的方法来处理文本。正则表达式的全面模式匹配表示法使您可以快速分析大量文本以找到特定的字符模式;提取、编辑、替换或删除文本子字符串;或将提取的字符串添加到集合以生成报告。对于处理字符串(例如 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 = 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");

说明,其实关于正则表达式还有比较多的内容可以讲,比如反向引用、匹配顺序及几种匹配模式的区别和联系等,不过这些在日常开发中使用不是太多(如果做文本分析处理还是会用到的),所以暂时不会继续讲了。尽管本系列四篇文章篇幅都不是太长(本人不敢太熬夜了,因为每天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月份天数的不同的情况写了一个相当复杂的正则表达式来验证,个人觉得可以结合将文本值转换成日期的方式来共同验证,这样更好理解和接受些。

到此,关于正则表达式的文章就暂时写到这里了,其它还有一些知识用得不是太多,以后有时间再总结了,接下来我可能要比较一下ADO.NET与ORM。


















本文转自周金桥51CTO博客,原文链接: http://blog.51cto.com/zhoufoxcn/283021,如需转载请自行联系原作者








相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
7月前
|
开发框架 前端开发 .NET
C#编程与Web开发
【4月更文挑战第21天】本文探讨了C#在Web开发中的应用,包括使用ASP.NET框架、MVC模式、Web API和Entity Framework。C#作为.NET框架的主要语言,结合这些工具,能创建动态、高效的Web应用。实际案例涉及企业级应用、电子商务和社交媒体平台。尽管面临竞争和挑战,但C#在Web开发领域的前景将持续拓展。
215 3
|
7月前
|
SQL 开发框架 安全
C#编程与多线程处理
【4月更文挑战第21天】探索C#多线程处理,提升程序性能与响应性。了解C#中的Thread、Task类及Async/Await关键字,掌握线程同步与安全,实践并发计算、网络服务及UI优化。跟随未来发展趋势,利用C#打造高效应用。
204 3
|
1月前
|
C# 开发者
C# 一分钟浅谈:Code Contracts 与契约编程
【10月更文挑战第26天】本文介绍了 C# 中的 Code Contracts,这是一个强大的工具,用于通过契约编程增强代码的健壮性和可维护性。文章从基本概念入手,详细讲解了前置条件、后置条件和对象不变量的使用方法,并通过具体代码示例进行了说明。同时,文章还探讨了常见的问题和易错点,如忘记启用静态检查、过度依赖契约和性能影响,并提供了相应的解决建议。希望读者能通过本文更好地理解和应用 Code Contracts。
35 3
|
1月前
|
设计模式 C# 图形学
Unity 游戏引擎 C# 编程:一分钟浅谈
本文介绍了在 Unity 游戏开发中使用 C# 的基础知识和常见问题。从 `MonoBehavior` 类的基础用法,到变量和属性的管理,再到空引用异常、资源管理和性能优化等常见问题的解决方法。文章还探讨了单例模式、事件系统和数据持久化等高级话题,旨在帮助开发者避免常见错误,提升游戏开发效率。
50 4
|
3月前
|
API C#
C# 一分钟浅谈:文件系统编程
在软件开发中,文件系统操作至关重要。本文将带你快速掌握C#中文件系统编程的基础知识,涵盖基本概念、常见问题及解决方法。文章详细介绍了`System.IO`命名空间下的关键类库,并通过示例代码展示了路径处理、异常处理、并发访问等技巧,还提供了异步API和流压缩等高级技巧,帮助你写出更健壮的代码。
50 2
|
2月前
|
安全 C# 数据安全/隐私保护
实现C#编程文件夹加锁保护
【10月更文挑战第16天】本文介绍了两种用 C# 实现文件夹保护的方法:一是通过设置文件系统权限,阻止普通用户访问;二是使用加密技术,对文件夹中的文件进行加密,防止未授权访问。提供了示例代码和使用方法,适用于不同安全需求的场景。
137 0
|
3月前
|
安全 程序员 编译器
C#一分钟浅谈:泛型编程基础
在现代软件开发中,泛型编程是一项关键技能,它使开发者能够编写类型安全且可重用的代码。C# 自 2.0 版本起支持泛型编程,本文将从基础概念入手,逐步深入探讨 C# 中的泛型,并通过具体实例帮助理解常见问题及其解决方法。泛型通过类型参数替代具体类型,提高了代码复用性和类型安全性,减少了运行时性能开销。文章详细介绍了如何定义泛型类和方法,并讨论了常见的易错点及解决方案,帮助读者更好地掌握这一技术。
83 11
|
3月前
|
SQL 开发框架 安全
并发集合与任务并行库:C#中的高效编程实践
在现代软件开发中,多核处理器普及使多线程编程成为提升性能的关键。然而,传统同步模型在高并发下易引发死锁等问题。为此,.NET Framework引入了任务并行库(TPL)和并发集合,简化并发编程并增强代码可维护性。并发集合允许多线程安全访问,如`ConcurrentQueue&lt;T&gt;`和`ConcurrentDictionary&lt;TKey, TValue&gt;`,有效避免数据不一致。TPL则通过`Task`类实现异步操作,提高开发效率。正确使用这些工具可显著提升程序性能,但也需注意任务取消和异常处理等常见问题。
55 1
|
3月前
|
安全 数据库连接 API
C#一分钟浅谈:多线程编程入门
在现代软件开发中,多线程编程对于提升程序响应性和执行效率至关重要。本文从基础概念入手,详细探讨了C#中的多线程技术,包括线程创建、管理及常见问题的解决策略,如线程安全、死锁和资源泄露等,并通过具体示例帮助读者理解和应用这些技巧,适合初学者快速掌握C#多线程编程。
88 0