c#

位置:IT落伍者 >> c# >> 浏览文章

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


发布日期:2024年01月05日
 
C#正则表达式编程(四):正则表达式

提供了功能强大灵活而又高效的方法来处理文本正则表达式的全面模式匹配表示法使您可以快速分析大量文本以找到特定的字符模式提取编辑替换或删除文本子字符串或将提取的字符串添加到集合以生成报告对于处理字符串(例如 HTML 处理日志文件分析和 HTTP 标头分析)的许多应用程序而言正则表达式是不可缺少的工具正则表达式是一个非常有用的技术有人曾称之为能让程序员不至于丢掉饭碗的十大技术之一可见它的重要性

熟悉DOS或者命令行的朋友或许已经用过类似的功能比如我们要查找D盘下所有的低于Word版本的Word文件(因为低于Word版本的Word文件的文件后缀是doc而Word版本的Word文件的文件后缀是docx)我们可以在命令行下执行这个命名

dir D:\*doc

当然如果想查找D盘下任意级子目录下的所有此类文件就应该执行dir /s D:\*doc了

注意正则表达式并不是在中独有的东东实际上在其它语言中早就实现了比如(可能很多人没有听说过这个编程语言十年前大学期间我曾经学过一点皮毛)其它的编程语言及等也支持正则表达式正则表达式差不多像SQL语言一样成为标准了同样和SQL类似在不同的厂商那里对SQL标准支持的程度并不完全一样正则表达式也是如此大部分内的正则表达式可以跨语言使用但是在各语言中也会有细微的区别这一点是需要我们注意的

正则表达式元字符

正则表达式语言由两种基本字符类型组成原义(正常)文本字符和元字符元字符使正则表达式具有处理能力元字符既可以是放在[]中的任意单个字符(如[a]表示匹配单个小写字符a)也可以是字符序列(如[ad]表示匹配abcd之间的任意一个字符而\w表示任意英文字母和数字及下划线)下面是一些常见的元字符

元字符 说明

匹配除 \n 以外的任何字符(注意元字符是小数点)

[abcde]

匹配 abcde之中的任意一个字符

[ah] 匹配a到h之间的任意一个字符

[^fgh] 不与fgh之中的任意一个字符匹配

\w 匹配大小写英文字符及数字之间的任意一个及下划线相当于[azAZ_]

\W 不匹配大小写英文字符及数字之间的任意一个相当于[^azAZ_]

\s

匹配任何空白字符相当于[ \f\n\r\t\v]

\S 匹配任何非空白字符相当于[^\s]

\d 匹配任何之间的单个数字相当于[]

\D 不匹配任何之间的单个数字相当于 [^]

[\ue\ufa] 匹配任意单个汉字(这里用的是Unicode编码表示汉字的)

正则表达式限定符

上面的元字符都是针对单个字符匹配的要想同时匹配多个字符的话还需要借助限定符下面是一些常见的限定符(下表中n和m都是表示整数并且<n<m)

限定浮 说明

* 匹配到多个元字符相当于{}

? 匹配个元字符相当于{}

{n} 匹配n个元字符

{n} 匹配至少n个元字符

{nm} 匹配n到m个元字符

+ 匹配至少个元字符相当于{}

\b 匹配单词边界

^ 字符串必须以指定的字符开始

{$selection}nbsp; 字符串必须以指定的字符结束

说明

)由于在正则表达式中\?*^$+|{[等字符已经具有一定特殊意义如果需要用它们的原始意义则应该对它进行转义例如希望在字符串中至少有一个\那么正则表达式应该这么写\\+

)可以将多个元字符或者原义文本字符用括号括起来形成一个分组比如^()[]\d{}$表示任意以开头的移动号码

)另外对于中文字符的匹配是采用其对应的 Unicode编码来匹配的对于单个Unicode字符如\ue表示汉字 \ufa表示汉字在Unicode编码中这分别是所能表示的汉字的第一个和最后一个的Unicode编码在Unicode编码中能表示 个汉字

)关于\b的用法它代表单词的开始或者结尾以字符串a b d作为示例字符串如果正则表达式是\b\d{}\b则仅能匹配

)可以使用|来表示或的关系例如 [z|j|q]表示匹配zjq之中的任意一个字母

正则表达式分组

将正则表达式的一部分用()括起来就可以形成一个分组也叫一个子匹配或者一个捕获组例如对于::这样格式的时间我们可以写如下的正则表达式

(([])|([])|([])([][]){}

如果以这个作为表达式它将从下面的一段IIS访问日志中提取出访问时间(当然分析IIS日志最好的工具是Log Parser这个微软提供的工具)

:: GET /admin_save

:: GET /userbudingasp

:: GET /upfile_flashasp

:: GET /cpphp

:: GET /sqldataphp

:: GET /

:: GET /l

如果我们想对上面的IIS日志进行分析提取每条日志中的访问时间访问页面客户端IP及端响应代码(对应C#中的HttpStatusCode)我们可以按照分组的方式来获取

代码如下

private String text= @:: GET /admin_saveasp

:: GET /userbudingasp

:: GET /upfile_flashasp

:: GET /cpphp

:: GET /sqldataphp

:: GET /

:: GET /l ;

/// <summary>

/// 分析IIS日志提取客户端访问的时间URLIP地址及服务器响应代码

/// </summary>

public void AnalyzeIISLog()

{

//提取访问时间URLIP地址及服务器响应代码的正则表达式

//大家可以看到关于提取时间部分的子表达式比较复杂因为做了比较严格的时间匹配限制

//注意为了简化起见没有对客户端IP格式进行严格验证因为 IIS访问日志中也不会出现不符合要求的IP地址

Regex regex = new Regex(@(([]|[]|[])([][]){})\s(GET)\s([^\s]+)\s(\d{}(\\d{}){})\s(\d{}) RegexOptionsNone)

MatchCollection matchCollection = regexMatches(text)

for (int i = ; i < matchCollectionCount; i++)

{

Match match = matchCollection[i];

ConsoleWriteLine(Match[{}]======================== i)

for (int j = ; j < matchGroupsCount; j++)

{

ConsoleWriteLine(Groups[{}]={} j matchGroups[j]Value)

}

}

}

这段代码的输出结果如下

Match[]========================

Groups[]=:: GET /admin_saveasp

Groups[]=::

Groups[]=

Groups[]=:

Groups[]=GET

Groups[]=/admin_saveasp

Groups[]=

Groups[]=

Groups[]=

Match[]========================

Groups[]=:: GET /userbudingasp

Groups[]=::

Groups[]=

Groups[]=:

Groups[]=GET

Groups[]=/userbudingasp

Groups[]=

Groups[]=

Groups[]=

Match[]========================

Groups[]=:: GET /upfile_flashasp

Groups[]=::

Groups[]=

Groups[]=:

Groups[]=GET

Groups[]=/upfile_flashasp

Groups[]=

Groups[]=

Groups[]=

Match[]========================

Groups[]=:: GET /cpphp

Groups[]=::

Groups[]=

Groups[]=:

Groups[]=GET

Groups[]=/cpphp

Groups[]=

Groups[]=

Groups[]=

Match[]========================

Groups[]=:: GET /sqldataphp

Groups[]=::

Groups[]=

Groups[]=:

Groups[]=GET

Groups[]=/sqldataphp

Groups[]=

Groups[]=

Groups[]=

Match[]========================

Groups[]=:: GET /

Groups[]=::

Groups[]=

Groups[]=:

Groups[]=GET

Groups[]=/

Groups[]=

Groups[]=

Groups[]=

Match[]========================

Groups[]=:: GET /l

Groups[]=::

Groups[]=

Groups[]=:

Groups[]=GET

Groups[]=/l

Groups[]=

Groups[]=

Groups[]=

从上面的输出结果中我们可以看出在每一个匹配结果中个分组就是客户端访问时间(因为索引是从开始的所以索引顺序为以下同理)个分组是访问的URL(索引顺序为个分组是客户端IP(索引顺序为个分组是服务器端响应代码(索引顺序为如果我们要提取这些元素可以直接按照索引来访问这些值就可以了这样比我们不采用正则表达式要方便多了

命名捕获组

上面的方法尽管方便但也有一些不便之处假如需要提取更多的信息对捕获组进行了增减就会导致捕获组索引对应的值发生变化我们就需要重新修改代码这也算是一种硬编码吧有没有比较好的办法呢?答案是有的那就是采用命名捕获组

就像我们使用 DataReader访问数据库或者访问DataTable中的数据一样可以使用索引的方式(索引同样也是从开始)不过如果变化了select语句中的字段数或者字段顺序按照这种方式获取数据就需要重新变动为了适应这种变化同样也允许使用字段名作为索引来访问数据只要数据源中存在这个字段而不管顺序如何都会取到正确的值在正则表达式中命名捕获组也可以起到同样的作用

普通捕获组表示方式(正则表达式)如(\d{})

命名捕获组表示方式(?<捕获组命名>正则表达式)如(?<phone>\d{})

对于普通捕获组只能采用索引的方式获取它对应的值但对于命名捕获组还可以采用按名称的方式访问例如(?<phone>\d{})在代码中就可以按照 matchGroups[phone]的方式访问这样代码更直观编码也更灵活针对刚才的对IIS日志的分析我们采用命名捕获组的代码如下

private String text= @:: GET /admin_saveasp

:: GET /userbudingasp

:: GET /upfile_flashasp

:: GET /cpphp

:: GET /sqldataphp

:: GET /

:: GET /l ;

/// <summary>

/// 采用命名捕获组提取IIS日志里的相关信息

/// </summary>

public void AnalyzeIISLog()

{

Regex regex = new Regex(@(?<time>([]|[]|[])([][]){})\s(GET)\s(?<url>[^\s]+)\s(?<ip>\d{}(\\d{}){})\s(?<httpCode>\d{}) RegexOptionsNone)

MatchCollection matchCollection = regexMatches(text)

for (int i = ; i < matchCollectionCount; i++)

{

Match match = matchCollection[i];

ConsoleWriteLine(Match[{}]======================== i)

ConsoleWriteLine(time:{} matchGroups[time])

ConsoleWriteLine(url:{} matchGroups[url])

ConsoleWriteLine(ip:{} matchGroups[ip])

ConsoleWriteLine({} matchGroups[httpCode])

}

}

这段代码的执行效果如下

Match[]========================

time:::

url:

ip:

httpCode:

Match[]========================

time:::

url:

ip:

httpCode:

Match[]========================

time:::

url:

ip:

httpCode:

Match[]========================

time:::

url:

ip:

httpCode:

Match[]========================

time:::

url:

ip:

httpCode:

Match[]========================

time:::

url:

ip:

httpCode:

Match[]========================

time:::

url:

ip:

httpCode:

采用命名捕获组之后使访问捕获组的值更直观了而且只要命名捕获组的值不发生变化其它的变化都不影响原来的代码

非捕获组

如果经常看别人有关正则表达式的源代码可能会看到形如(? 子表达式)这样的表达式这就是非捕获组对于捕获组我们可以理解就是在后面的代码中可以通过索引或者名称(如果是命名捕获组)的方式来访问匹配的值因为在匹配过程中会将对应的值保存到中如果我们在后面不需要访问匹配的值那么就可以告诉程序不用在内存中保存匹配的值以便提高效率减少内存消耗这种情况下就可以使用非捕获组例如在刚刚分析IIS日志的时候我们对客户端提交请求的方式并不在乎在这里就可以使用非捕获组如下

Regex regex = new Regex(@(?<time>([]|[]|[])([][]){})\s(?GET)\s(?<url>[^\s]+)\s(?<ip>\d{}(\\d{}){})\s(?<httpCode>\d{});

零宽度断言

关于零宽度断言有多种叫法也有叫环视也有叫预搜索的我这里采用的是MSDN中的叫法关于零宽度断言有以下几种

(?= 子表达式)零宽度正预测先行断言仅当子表达式在此位置的右侧匹配时才继续匹配例如(?=) 与跟在前面的实例匹配

(?! 子表达式)零宽度负预测先行断言仅当子表达式不在此位置的右侧匹配时才继续匹配例如(?!)与不以结尾的单词匹配所以不与匹配

(?<= 子表达式)零宽度正回顾后发断言仅当子表达式在此位置的左侧匹配时才继续匹配例如(?<= 与跟在 后面的 的实例匹配此构造不会回溯

(?<! 子表达式)零宽度负回顾后发断言仅当子表达式不在此位置的左侧匹配时才继续匹配例如(?<=)与不以开头的单词匹配所以不与 匹配

正则表达式选项

在使用正则表达式时除了使用RegexOptions这个枚举给正则表达式赋予一些额外的选项之外还可以在在表达式中使用这些选项

Regex regex = new Regex((?i)def

Regex regex = new Regex((?i)def

它与下面一句是等效的

Regex regex = new Regex(def RegexOptionsIgnoreCase)

Regex regex = new Regex(def RegexOptionsIgnoreCase)

采用(?i)这种形式的称之为内联模式顾名思义就是在正则表达式中已经体现了正则表达式选项这些内联字符与RegexOptions的对应如下

IgnoreCase:内联字符为i指定不区分大小写的匹配

Multiline:内联字符为m指定多行模式更改 ^ 和 $ 的含义以使它们分别与任何行的开头和结尾匹配而不只是与整个字符串的开头和结尾匹配

ExplicitCapture:内联字符为n指定唯一有效的捕获是显式命名或编号的 (?<name>…) 形式的组这允许圆括号充当非捕获组从而避免了由 (?…) 导致的语法上的笨拙

Singleline:内联字符为s指定单行模式更改句点字符 () 的含义以使它与每个字符(而不是除 \n 之外的所有字符)匹配

IgnorePatternWhitespace:内联字符为x指定从模式中排除非转义空白并启用数字符号 (#) 后面的注释(有关转义空白字符的列表请参见字符转义) 请注意空白永远不会从字符类中消除

举例说明

RegexOptions option=RegexOptionsIgnoreCase|RegexOptionsSingleline;

Regex regex = new Regex(def option)

用内联的形式表示为

Regex regex = new Regex((?is)def

说明其实关于正则表达式还有比较多的内容可以讲比如反向引用匹配顺序及几种匹配模式的区别和联系等不过这些在日常开发中使用不是太多(如果做文本分析处理还是会用到的)所以暂时不会继续讲了尽管本系列四篇文章篇幅都不是太长(本人不敢太熬夜了因为每天点多就要起床)不过通过这些基础的学习仍是可以掌握正则表达式的精华之处的至于在开发中怎么样去用就要靠我们自己灵活去结合实际情况用了我个人经验是如果是用于验证是否满足要求那么写正则表达式时写得严格一点如果是从规范格式的文本中提取数据则可以写得宽松一点比如验证时间则必须写成(?<time>([]|[]|[])( [][]){})这种形式这样别人输入::就不能通过验证但是如果是像从上面提到的IIS日志中提取时间用 (?<time>\d{}(\d{}){})这种方式也是可以当然如果写比较严格的验证比较麻烦时也可以写比较宽松的格式然后借助其它手段来验证在网上有一个验证日期的正则表达式编写者充分考虑到各个月份天数的不同甚至平年和闰年月份天数的不同的情况写了一个相当复杂的正则表达式来验证个人觉得可以结合将文本值转换成日期的方式来共同验证这样更好理解和接受些

到此关于正则表达式的文章就暂时写到这里了其它还有一些知识用得不是太多以后有时间再总结了

               

上一篇:ADO.NET实用经验与大家分享

下一篇:.NET Framework经验总结