01C#正则表达式编程

C#正则表达式编程(一):C#中有关 正则的类

正则表达式是一门灵活性非常强的语言,匹配同样的字符串可能在不同的开发人员那里会得到不同的结果,在平常的时候也是用的时候看看相关资料,不用的时候就丢在脑后了,尽管在处理大部分情况下都能迅速处理,但是处理一些复杂的情况效率仍是不高,借着前阵子做过的一个项目涉及到正则表达式的机会,将 有关资料阅读了一遍并结合了自己的体会,整理了几篇利用C#进行正则表达式编程的文章,一来加深自己的印象和理解,二来供博客上的读者学习借鉴。
在.NET 中提供了对正则表达式的支持,并且提供了相关的类,分别有:Regex、Match、Group、Capture、RegexOptions、 MatchCollection、GroupCollection、CaptureCollection。它们之间的关联如下:

对它们描述如下:
Regex:正则表达式类,代表了一个不可变的正则表达式。
Match:代表了 Regex类的实例的一次匹配结果,可以通过Regex的Match()实例方法返回一个Match的实例。
MatchCollection:代 表了Regex类的实例的所有匹配结果,可以通过Regex的Matches()实例方法返回一个MatchCollection的实例。
Group: 表示单个捕获组的结果。由于一次匹配可能包含0个、1个或多个分组,所以Match的实例中返回的是捕获组集合的结果,即 GroupCollection。
GroupCollection:表示单个匹配中的多个捕获组的集合,可以通过Match的Groups实例属 性返回GroupCollection的实例。
Capture:表示单个捕获中的一个子字符串。同Group一样,由于一个捕获中可能包含0个、 1个或多个子字符串,所以Group的实例中返回的是子字符串集合的结果,即CaptureCollection。
CaptureCollection: 默认表示按照从里到外、从左到右的顺序由捕获组匹配到的所有子字符串集合,可以通过Group或者Match的Captures实例属性返回 CaptureCollection的实例。注意,可以使用RegexOptions.RightToLeft来改变这种匹配顺序。
RegexOptions: 提供用于设置正则表达式选项的枚举值。 像上面提到的RightToLeft就是它的一个枚举值之一,除此之外还有None、IgnoreCase、Multiline、 ExplicitCapture、Compiled、Singleline、IgnorePatternWhitespace、RightToLeft、 ECMAScript及CultureInvariant。RegexOptions枚举值可以相加,比如我们想匹配不区分大小写的字符串“abc”并且 还想提高一下执行速度,那么可以写如下代码:
RegexOptions options=RegexOptions.IgnoreCase|RegexOptions.Compiled;
Regex regex=new Regex("abc",options);

Regex、Match、Group及Capture的关系及成员

从上图可以看出Regex类提供了许多静态方法,很多方法还提供了多种重载方式(在图中对存在多种参数重载的方法都以 “...”表示),除此之外我们还会发现Capture、Group及Match之间存在继承关系(说实在话刚开始用的时候我发现它们之间存在着很多相同 的字段,这让我当时迷惑不已,希望大家看到这个图后不要再像我当初那样迷惑了)。
在使用C#中的正则表达式进行文本处理之前先花点时间了解一 下.NET中有关正则表达式的类和它们之间的关系是有必要的,这篇就算是预热篇了,在开始学习正则表达式之前做做热身运动。虽然在C#中有关正则表达式的 类不多,但是对于初学者来说还是容易引起混淆,从而出现不知道该用哪些类的哪些方法或者属性的情况,这篇算是做个初步介绍吧。下一篇就先讲述Regex 类,利用Regex可以用来替换、分割和处理字符串。

 

C#正则表达式编程(二):Regex 类用法

上一篇讲述了在C#中有关正则表达式的类之间的关系,以及它们的 方法,这一篇主要是将Regex这个类的用法的,关于Match及MatchCollection类会在下一篇讲到。

对于正则表达式的应用, 基本上可以分为验证、提取、分割和替换。仅仅利用Regex类就可以实现验证和简单替换。

利用Regex类实现验证

经历2009年的备案和DNS停止解析风波之后,大部分的带有反馈性的网站 和论坛都对一些敏感词进行了过滤,包含有这类敏感词的文章要么内容被替换要么被禁止发表,利用Regex类就可以实现这个功能,下面是一个例子:

/// <summary>

/// 检查字符串中是否有孙权这个敏感词

/// </summary>

public void IsMatchDemo()

{

        string source = "刘备ABC关羽ABc张飞Abc赵云abc诸葛亮aBC孙权abC周瑜AbC鲁肃aBc曹操许攸郭嘉需晃袁绍";

        Regex regex = new Regex("孙权");

        //if (Regex.IsMatch(source, "孙权"))

        //下面这句和上面被注释掉的一句作用的同样的

        if (regex.IsMatch(source))

        {

                Console.WriteLine("字符串 中包含有敏感词:孙权!");

        }

输出结果:字符串中包含 有敏感词:孙权!

对于上面的例子,如果要 检查的字符串中包含“孙权”这个关键词就会在控制台上输出提示,当然在实际的应用中可能是包含有被禁止的词语的内容不允许提交而不是仅仅提示了。不过这类 情况仍有办法可以绕过,可以使用“孙-权”或“孙+权”来替换孙权从而来绕过验证。

对于中文字符串还比较好 说,对于英文的字符串还要考虑每个字母的大小写情况了。比如我们禁止在内容中出现某个关键词(如太CCTV的CCTV,或者CCAV),难道我们要针对字符串中每个字母的大小写情况进行多种情况的组合验证?不,完全没有必要,下面就是一个例子:

    

/// <summary>

/// 检查字符串中是否有“def”的任何大小写形式

/// </summary>

public void IsMatchDemoWithOption()

{

        string source = "刘备ABC关羽ABc张飞Abc赵云abc诸葛亮aBC孙权abC周瑜AbC鲁肃aBc曹操DEF许攸郭嘉需晃袁绍";

        Regex regex = new Regex("def",RegexOptions.IgnoreCase);

        if (regex.IsMatch(source))

        {

                Console.WriteLine("字符串 中包含有敏感词:def");

        }

}

输出结果:字符串中包含 有敏感词:def!

 

在上面的例子中,实例化Regex时采用了两个带参数的构造函数,其中第二个参 数就是上一篇中提到的RegexOptions枚举,RegexOptions.IgnoreCase表示匹配字符串的时候不管大小写是否一致。

此外,在Regex中存在着一些功能相同的静态方法和实例方法, 如:IsMatch()方法,在第一个例子中我 还写出了两种方法的实例,如下:

Regex regex = new Regex("孙权");

//if (Regex.IsMatch(source, "孙权"))

//下面这句和上面被注释掉的一句作用的同样的

if (regex.IsMatch(source))

其实在.NET Framework中很多类都有这样类似的情况,在System.IO命名空间下还有File及FileInfo这样的静态类和非静态类的情况,其实它们提供 了相似的功能,用小沈阳的话说“这是为什么呢”?有部分是出自效率的考虑,并且也有出自让代码编写方便和看起来简洁的因素。对于偶尔一半次为之的情况,建 议使用静态方法,这样有可能会提高效率(因为采用静态方法调用的正则表达式会被内部缓存,默认情况下会缓存15个,可以通过设置Regex类的CacheSize属性来更改缓存个数),如果是要在循环中多次 使用,那就采用实例方法吧。

使用Regex类进行替换

上面的处理仅仅是查看提 交的内容中是否有被禁止的关键词,其实有时候还可以做到将被禁止的关键词进行替换,例如将上面用到的字符串中的任何形式的"ABC"替换成"|",下面就是一个例子:

/// <summary>

/// 实现字符串替换功能

/// </summary>

public void Replace()

{

        string source = "刘备ABC关羽ABc张飞Abc赵云abc诸葛亮aBC孙权abC周瑜AbC鲁肃aBc曹操DEF许攸郭嘉需晃袁绍";

        Regex regex = new Regex("abc", RegexOptions.IgnoreCase);

        string result=regex.Replace(source, "|");

        Console.WriteLine("原始字符串:" + source);

        Console.WriteLine("替换后的字符串:" + result);

}

输出结果:

原始字符串:刘备ABC关羽ABc张飞Abc赵云abc诸葛亮aBC孙权abC周瑜AbC鲁肃aBc曹操DEF许攸郭嘉需晃袁绍

替换后的字符串:刘备|关羽|张飞|赵云|诸葛亮|孙权|周瑜|鲁肃|曹操DEF许攸郭嘉需晃袁绍

实际上有时候我们遇到的 情况可能不仅仅这么简单,例如有时候我们希望将字符串中的任何形式的“ABC”及“DEF”实现HTML形式的加粗,也就是替换成<b>abc</b>及<b>def</b>这种形式,当然还保持和原来一致的大小写形式,代码如下:

/// <summary>

/// 实现字符串替换功能

/// </summary>

public void ReplaceMatchEvaluator()

{

        string source = "刘备ABC关羽ABc张飞Abc赵云abc诸葛亮aBC孙权abC周瑜AbC鲁肃aBc曹操DEF许攸郭嘉需晃袁绍";

        Regex regex = new Regex("[A-Z]{3}", RegexOptions.IgnoreCase);

        string result = regex.Replace(source, new MatchEvaluator(OutPutMatch));

        Console.WriteLine("原始字符串:" + source);

        Console.WriteLine("替换后的字符串:" + result);

}

/// <summary>

/// MatchEvaluator委托中调用的方法,可以对匹配结果进行处理

/// </summary>

/// <param name="match">操作过程中的单个正则表达式匹配</param>

/// <returns></returns>

private string OutPutMatch(Match match)

{

        return "<b>" + match.Value + "</b>";

}

输出结果如下:

原始字符串:刘备ABC关羽ABc张飞Abc赵云abc诸葛亮aBC孙权abC周瑜AbC鲁肃aBc曹操DEF许攸郭嘉需晃袁绍

替换后的字符串:刘备<b>ABC</b>关羽<b>ABc</b>张飞<b>Abc</b>赵云<b>abc</b>诸葛亮<b>aBC</b>孙权<b>abC</b>周瑜<b>AbC</b>鲁肃<b>aBc</b>曹操<b>DEF</b>许攸郭嘉需晃袁绍

在上面的例子中,我们使 用了MatchEvaluator委托,并且还涉及到了Match类(Match类将会在下一篇讲述),在MatchEvaluator委托中使用到的Match类表示了单个的正则表达式匹配,通过改变match实例中Value的值来达到完成替换的目的。

在本篇中仅仅是讲述了Regex类的一些简单用法,也没有讲述正则表达式的相 关知识,不过即使如此也能减轻我们的一部分工作,学习和灵活运用正则表达式是一个长期积累的过程。

 

 

C#正则表达式编程(三):Match 类和Group类用法

前面两篇讲述了正则表达式的基础和一些简单的例子,这篇将稍微深入一点探讨一下正则表达式分组,在.NET中正则表达式分组是用Math类来代表的。
首 先先看一段代码:

/// <summary>
/// 显示Match内多个Group的例子
/// </summary>
public void ShowStructure()
{
        //要匹配的字符串
        string text = "1A 2B 3C 4D 5E 6F 7G 8H 9I 10J 11Q 12J 13K 14L 15M 16N ffee80 #800080";
        //正则表达式
        string pattern = @"((\d+)([a-z]))\s+";
        //使用RegexOptions.IgnoreCase枚举值 表示不区分大小写
        Regex r = new Regex(pattern, RegexOptions.IgnoreCase);
        //使用正则表达式匹配字符串,仅返回一次匹配结果
        Match m = r.Match(text);
        while (m.Success)
        {

                //显示匹配开始处的索引值和匹配到的
                System.Console.WriteLine("Match=[" + m + "]");
                CaptureCollection cc = m.Captures;
                foreach (Capture c in cc)
                {
                        Console.WriteLine("\tCapture=[" + c + "]");
                }
                for (int i = 0; i < m.Groups.Count; i++)
                {
                        Group group = m.Groups[i];
                        System.Console.WriteLine("\t\tGroups[{0}]=[{1}]", i, group);
                        for (int j = 0; j < group.Captures.Count; j++)
                        {
                                Capture capture = group.Captures[j];
                                Console.WriteLine("\t\t\tCaptures[{0}]=[{1}]", j, capture);
                        }
                }
                //进行下一次匹配.
                m = m.NextMatch();
        }
}


这段代码的执行效果如下:
Match=[1A ]
    Capture=[1A ]
        Groups[0]=[1A ]
            Captures[0]=[1A ]
        Groups[1]=[1A]
            Captures[0]=[1A]
        Groups[2]=[1]
            Captures[0]=[1]
        Groups[3]=[A]
            Captures[0]=[A]
Match=[2B ]
    Capture=[2B ]
        Groups[0]=[2B ]
            Captures[0]=[2B ]
        Groups[1]=[2B]
            Captures[0]=[2B]
        Groups[2]=[2]
            Captures[0]=[2]
        Groups[3]=[B]
            Captures[0]=[B]
..................此去省略一些结果
Match=[16N ]
    Capture=[16N ]
        Groups[0]=[16N ]
            Captures[0]=[16N ]
        Groups[1]=[16N]
            Captures[0]=[16N]
        Groups[2]=[16]
            Captures[0]=[16]
        Groups[3]=[N]
            Captures[0]=[N]
通过对上面的代码结合代码的分析,我们得出下面的结论,在((\d+)([a-z]))\s+这个正则表达式里 总共包含了四个Group,即分组,按照默认的从左到右的匹配方式,其中Groups[0]代表了整个分组,其它的则是子分组,用示意图表示如下:

在上面的代码中是采用了Regex类的Match()方法,调用这种方法返回的是一个Match,要处 理分析全部的字符串,还需要在while循环的中通过Match类的NextMatch()方法返回下一个可能成功的匹配(可通过Match类的 Success属性来判断是否成功匹配)。上面的代码还可以写成如下形式:

/// <summary>
/// 使用Regex类的Matches方法所有所有的匹配
/// </summary>
public void Matches()
{
        //要匹配的字符串
        string text = "1A 2B 3C 4D 5E 6F 7G 8H 9I 10J 11Q 12J 13K 14L 15M 16N ffee80 #800080";
        //正则表达式
        string pattern = @"((\d+)([a-z]))\s+";
        //使用RegexOptions.IgnoreCase枚举值 表示不区分大小写
        Regex r = new Regex(pattern, RegexOptions.IgnoreCase);
        //使用正则表达式匹配字符串,返回所有的匹配结果
        MatchCollection matchCollection = r.Matches(text);
        foreach (Match m in matchCollection)
        {
                //显示匹配开始处的索引值和匹配到的值
                System.Console.WriteLine("Match=[" + m + "]");
                CaptureCollection cc = m.Captures;
                foreach (Capture c in cc)
                {
                        Console.WriteLine("\tCapture=[" + c + "]");
                }
                for (int i = 0; i < m.Groups.Count; i++)
                {
                        Group group = m.Groups[i];
                        System.Console.WriteLine("\t\tGroups[{0}]=[{1}]", i, group);
                        for (int j = 0; j < group.Captures.Count; j++)
                        {
                                Capture capture = group.Captures[j];
                                Console.WriteLine("\t\t\tCaptures[{0}]=[{1}]", j, capture);
                        }
                }
        }
}


上面的这段代码和采用While循环遍历所有匹配的结果是一样的,在实际情况中有可能出现不 需要全部匹配而是从某一个位置开始匹配的情况,比如从第32个字符处开始匹配,这种要求可以通过Match()或者Matches()方法的重载方法来实 现,仅需要将刚才的实例代码中的MatchCollection matchCollection = r.Matches(text);改为MatchCollection matchCollection = r.Matches(text,48);就可以了。

输出结果如下:
Match=[5M ]
        Capture=[5M ]
                Groups[0]=[5M ]
                        Captures[0]=[5M ]
                Groups[1]=[5M]
                        Captures[0]=[5M]
                Groups[2]=[5]
                        Captures[0]=[5]
                Groups[3]=[M]
                        Captures[0]=[M]
Match=[16N ]
        Capture=[16N ]
                Groups[0]=[16N ]
                        Captures[0]=[16N ]
                Groups[1]=[16N]
                        Captures[0]=[16N]
                Groups[2]=[16]
                        Captures[0]=[16]
                Groups[3]=[N]
                        Captures[0]=[N]
注意上面的 MatchCollection matchCollection = r.Matches(text,48)表示从text字符串的位置48处开始匹配,要注意位置0位于整个字符串的之前,位置1位于字符串中第一个字符之后 第二个字符之前,示意图如下(注意是字符串“1A”与“2B”之间有空格):

在text的位置48处正好是15M中的5处,因此返回的第一个Match是5M而不是15M。这里还 继续拿出第一篇中的图来,如下:

从上图可以看出Capture、Group及Match类之间存在继承关系,处在继承关系顶端的 Capture类中就定义了Index、Length和Value属性,其中Index表示原始字符串中发现捕获子字符串的第一个字符的出现位 置,Length属性表示子字符串的长度,而Value属性表示从原始字符串中捕获的子字符串,利用这些属性可以实现一些比较复杂的应用。例如在现在还有 很多论坛仍没有使用所见即所得的在线编辑器,而是使用了一种UBB编码的编辑器,使用所见即所得的编辑器存在着一定的安全风险,比如可以在源代码中嵌入 js代码或者其它恶意代码,这样浏览者访问时就会带来安全问题,而使用UBB代码就不会代码这个问题,因为UBB代码包含了有限的、但不影响常规使用的标 记并且支持UBB代码的编辑器不允许直接在字符串中出现HTML代码,也而就避免恶意脚本攻击的问题。在支持UBB代码的编辑器中输入的文本在存入数据库 中保存的形式是UBB编码,显示的时候需要将UBB编码转换成HTML代码,例如下面的一段代码就是UBB编码:
[url]http://zhoufoxcn.blog.51cto.com[/url] [url=http://blog.csdn.net/zhoufoxcn]周公的专栏[/url]
下面通过例子演示如何将上面的 UBB编码转换成HTML代码:

/// <summary>
/// 下面的代码实现将文本中的UBB超级链接代码替换为HTML超级链接代码
/// </summary>
public void UBBDemo()
{
        string text = "[url=http://zhoufoxcn.blog.51cto.com] [/url][url=http://blog.csdn.net/zhoufoxcn]周公的专栏[/url]";
        Console.WriteLine("原始UBB代码:" + text);
        Regex regex = new Regex(@"(\[url=([ \S\t]*?)\])([^[]*)(\[\/url\])", RegexOptions.IgnoreCase);
        MatchCollection matchCollection = regex.Matches(text);
        foreach (Match match in matchCollection)
        {
                string linkText = string.Empty;
                //如果包含了链接文字,如第二个UBB代码中 存在链接名称,则直接使用链接名称
                if (!string.IsNullOrEmpty(match.Groups[3].Value))
                {
                        linkText = match.Groups[3].Value;
                }
                else//否则使用链接作为链接名称
                {
                        linkText = match.Groups[2].Value;
                }
                text = text.Replace(match.Groups[0].Value, "<a href=\"" + match.Groups[2].Value + "\" target=\"_blank\">" + linkText + "</a>");
        }
        Console.WriteLine("替换后的代码:"+text);

}


程序执行结果如下:

原始UBB代码:[url=http://zhoufoxcn.blog.51cto.com][/url] [url=http://blog.csdn.net/zhoufoxcn]周公的专栏[/url]
替换后的代码:<a href="http://zhoufoxcn.blog.51cto.com" target="_blank">http://zhoufoxcn.blog.51cto.com</a><a href="http://blog.csdn.net/zhoufoxcn"target="_blank">周公的专栏</a>
上 面的这个例子就稍微复杂点,对于初学正则表达式的朋友来说,可能有点难于理解,不过没有关系,后面我会讲讲正则表达式。在实际情况下,可能通过 match.Groups[0].Value这种方式不太方便,就想在访问DataTable时写string name=dataTable.Rows[i][j]这种方式一样,一旦再次调整,这种通过索引的方式极容易出错,实际上我们也可以采用名称而不是索引的 放来来访问Group分组,这个也会在以后的篇幅中去讲。

 

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

正则表达式提供了功能强大、灵活而又高效的方法来处理文本。正则表达式的全面模式匹配表示法使您可以快速分析大量文本以找到特定的字符模式;提取、编辑、 替换或删除文本子字符串;或将提取的字符串添加到集合以生成报告。对于处理字符串(例如 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日志,提取客户端访问的时间、URLIP地址及服务器响应代码
/// </summary>
public void AnalyzeIISLog()
{
        //提取访问时间、URLIP地址及服务器响应代码的正则表达
        //大家可以看到关于提取时间部分的子表达式比较复杂,因为做了 比较严格的时间匹配限制
        //注意为了简化起见,没有对客户端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。

 

 

 

 

全面剖析C#正则表达式

        到目前为止,许多的编程语言和工具都包含对正则表达式的支持,当然.NET也不例外,.NET基础类库中包含有一个名称空间和一系列可以充分发挥规则表达式威力的类。

        正则表达式的知识可能是不少编程人员最烦恼的事儿了。如果你还没有规则表达式方面的知识的话,建议从正则表达式的基础知识入手。前参见 正则表达式语法。

 

        下面就来研究C#中的正则表达式,C#中的正则表达式包含在.NET基础雷库的一个名称空间下,这个名称空间就是System.Text.RegularExpressions。该名称空间包括8个类,1个枚举,1个委托。他们分别是:

                     Capture: 包含一次匹配的结果;

                     CaptureCollection: Capture的序列;

                     Group: 一次组记录的结果,由Capture继承而来;

                     GroupCollection:表示捕获组的集合

                     Match: 一次表达式的匹配结果,由Group继承而来;

                     MatchCollection: Match的一个序列;

                     MatchEvaluator: 执行替换操作时使用的委托;

                     Regex:编译后的表达式的实例。

                     RegexCompilationInfo:提供编译器用于将正则表达式编译为独立程序集的信息

                     RegexOptions 提供用于设置正则表达式的枚举值

Regex类中还包含一些静态的方法:

                    Escape: 对字符串中的regex中的转义符进行转义;

                    IsMatch: 如果表达式在字符串中匹配,该方法返回一个布尔值;

                    Match: 返回Match的实例;

                    Matches: 返回一系列的Match的方法;

                    Replace: 用替换字符串替换匹配的表达式;

                    Split: 返回一系列由表达式决定的字符串;

                    Unescape:不对字符串中的转义字符转义。

 

下面介绍他们的用途:

        先看一个简单的匹配例子,我们首先从使用Regex、Match类的简单表达式开始学习。 Match m = Regex.Match("abracadabra", "(a|b|r)+"); 我们现在有了一个可以用于测试的Match类的实例,例如:if (m.Success){},如果想使用匹配的字符串,可以把它转换成一个字符串:   MesaageBox.Show("Match="+m.ToString()); 这个例子可以得到如下的输出: Match=abra。这就是匹配的字符串了。

 

        Regex 类表示只读正则表达式类。它还包含各种静态方法(在下面的实例中将逐一介绍),允许在不显式创建其他类的实例的情况下使用其他正则表达式类。

 

        以下代码示例创建了 Regex 类的实例并在初始化对象时定义一个简单的正则表达式。声明一个Regex对象变量:Regex objAlphaPatt;,接着创建Regex对象的一个实例,并定义其规则:objAlphaPatt=new Regex("[^a-zA-Z]");

 

        IsMatch方法指示 Regex 构造函数中指定的正则表达式在输入字符串中是否找到匹配项。这是我们使用C#正则表达式时最常用的方法之一。下面的例子说明了IsMatch方法的使用:

if( !objAlphaPatt.IsMatch("testisMatchMethod"))

 lblMsg.Text = "匹配成功";

else

 lblMsg.Text = "匹配不成功";

这段代码执行的结果是“匹配成功”

if( ! objAlphaPatt.IsMatch("testisMatchMethod7654298"))

 lblMsg.Text = "匹配成功";

else

 lblMsg.Text = "匹配不成功";

这段代码执行的结果是“匹配不成功”

 

         Escape方法表示把转义字符作为字符本身使用,而不再具有转义作用,最小的元字符集(\、*、+、?、|、{、[、(、)、^、$、.、# 和空白)。Replace方法则是用指定的替换字符串替换由正则表达式定义的字符模式的所有匹配项。看下面的例子,还是使用上面定义的Regex对象:objAlphaPatt.Replace("this [test] ** replace and escape" ,Regex.Escape("()"));他的返回结果是:this\(\)\(\)test\(\)\(\)\(\)\(\)\(\)replace\(\)and\(\)escape,如果不是Escape的话,则返回结果是:this()()test()()()()()replace()and()escape,Unescape 反转由 Escape 执行的转换,但是,Escape 无法完全反转 Unescape。

 

        Split方法是把由正则表达式匹配项定义的位置将输入字符串拆分为一个子字符串数组。例如:

Regex r = new Regex("-"); // Split on hyphens.

string[] s = r.Split("first-second-third");

for(int i=0;i<s.Length;i++)

{

 Response.Write(s[i]+"<br>");

}

 

执行的结果是:

First

Second

Third

 

        看上去和String的Split方法一样,但string的Split方法在由正则表达式而不是一组字符确定的分隔符处拆分字符串。

 

        Match方法是在输入字符串中搜索正则表达式的匹配项,并Regex 类的 Match 方法返回 Match 对象,Match 类表示正则表达式匹配操作的结果。下面的例子演示Match方法的使用,并利用Match对象的Group属性返回Group对象:

 

string text = @"public string testMatchObj string s string  match ";

string pat = @"(\w+)\s+(string)";

// Compile the regular expression.

Regex r = new Regex(pat, RegexOptions.IgnoreCase);

// Match the regular expression pattern against a text string.

Match m = r.Match(text);

int matchCount = 0;

while (m.Success)

{

 Response.Write("Match"+ (++matchCount) + "<br>");

 for (int i = 1; i <= 2; i++)

 {

  Group g = m.Groups[i];

  Response.Write("Group"+i+"='" + g + "'"  + "<br>");

  CaptureCollection cc = g.Captures;

  for (int j = 0; j < cc.Count; j++)

  {

   Capture c = cc[j];

   Response.Write("Capture"+j+"='" + c + "', Position="+c.Index + "<br>");

  }

 }

 m = m.NextMatch();

}

 

该事例运行结果是:

Match1

Group1='public'

Capture0='public', Position=0

Group2='string'

Capture0='string', Position=7

Match2

Group1='testMatchObj'

Capture0='testMatchObj', Position=14

Group2='string'

Capture0='string', Position=27

Match3

Group1='s'

Capture0='s', Position=34

Group2='string'

Capture0='string', Position=36

 

        MatchCollection 类表示成功的非重叠匹配的只读的集合,MatchCollection 的实例是由 Regex.Matches 属性返回的,下面的实例说明了通过在输入字符串中找到所有与Regex中指定的匹配并填充 MatchCollection。

 

MatchCollection mc;

Regex r = new Regex("match");

mc = r.Matches("matchcollectionregexmatchs");

for (int i = 0; i < mc.Count; i++)

{

 Response.Write( mc[i].Value + " POS:" + mc[i].Index.ToString() + "<br>");

}

该实例运行的结果是:

match POS:0

match POS:20

 

        正则表达式对初学者来说,往往是一种神秘的东西,实际上,仔细研究一下也就是那么回事儿,没有多么的难。希望这篇文章对你有所帮助。

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值