C#中正则表达式的应用简析

by shinichi_wtn 2010-08-23 17:11

正则表达式简介

在许多应用场景中,我们需要在字符串中查找特定的信息,或者编辑其中的部分信息。比如网页信息采集程序需要对HTML文本进行筛选和处理,获得锚点(<a>),列表(<li>)等等,如果没有正则表达式,我们需要编写繁琐的字符串处理程序来挖掘所需的文本;同样,要在一段程序代码中对关键词进行着色(代码着色问题),我们首先在字符串中找到关键词,然后把关键词替换为相应的HTML代码(如将class替换为<span style=”color:blue”>class</span>),如果没有正则表达式,这项工作将变得十分繁琐。因此,实现字符串灵活地搜索和替换是正则表达式的主要用途。

在《正则表达式必知必会》中这样形容道“正则表达式是一种威力无比强大的武器,可以完成各种复杂的文本处理工作,被称为程序员的‘瑞士军刀’”。的确,我曾经写过的爬虫程序核心就是对于HTML字符串的检索替换,方便从学校教务系统中采集课程信息。在自己尝试简单代码着色的过程中,更是大量运用了正则表达式。这篇文章主要是对我使用过的C#正则表达式进行一个系统的总结。文章中并不会介绍正则表达式的语法,而集中于用C#实现正则表达式的各种功能。

用C#编写正则表达式程序

.NET Framework提供了强大的专门用于实现正则表达式的类库,在调用时需要首先引用它

using System.Text.RegularExpressions;

要新建一个正则表达式,只需要创建一个Regex对象的实例,如

Regex myRegex = new Regex("<li>[^<]*</li>");

这个正则表达式寻找每个HTML的<li>标签及其内含的文本,如果现在有一段HTML代码需要我们处理,并将其赋值给inputString

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Test Page</title>
</head>
<body>
    <ul>
        <li>2006</li>
        <li>2007</li>
        <li>2008</li>
        <li>2009</li>
        <li>2010</li>
    </ul>
</body>
</html>

一旦我们调用

Regex myRegex = new Regex("<li>[^<]*</li>");
MatchCollection myMatches = myRegex.Matches(inputString);
foreach (Match myMatch in myMatches)
{
    Console.WriteLine(myMatch.Value);
}

就输出了所有匹配的项,输出结果如下

<li>2006</li>
<li>2007</li>
<li>2008</li>
<li>2009</li>
<li>2010</li>

这只是个简单的例子,下面将详细介绍Regex类所提供的功能

Match类

就如Match的意思一样,它表示了匹配,如果正则表达式查找成功,则会产生匹配。Regex类将匹配结果放在Match类。Regex提供了两个方法获取匹配结果,一个是Match方法(只返回第一个匹配),一个是Matches方法(返回所有匹配,存放在MatchCollection中),两者都需要传入被查询的字符串作为参数。

Match myMatch = myRegex.Match(inputString);
MatchCollection myMatches = myRegex.Matches(inputString);

一旦有了匹配,就能通过Value属性获取匹配的结果,同样可以通过Groups属性获取分组。值得注意的是,不管是否存在匹配,都能通过Regex的Match方法返回一个Match类实例,只是这个时候它的Success属性为Fasle。在实际应用中,我们可以先通过Regex提供的IsMatch方法判断是否有匹配,如果没有匹配,则没有必要新建一个Match类。

Regex myRegex = new Regex("<li>([^<]*)</li>");

if (myRegex.IsMatch(inputString))
{
    Match myMatch = myRegex.Match(inputString);
    while (myMatch.Success)
    {
        Console.WriteLine(myMatch.Groups[1].Value);
        myMatch = myMatch.NextMatch();
    }
}

为了最终匹配的结果不包含<li>标签,可以利用括号()对正则表达式进行分组,通过Match类的Groups属性获得想要的分组,就如上面的程序一样。值得注意的是,Groups属性的一个元素(Groups[0])并不是第一个分组,而是整个匹配字符串,真正的第一个分组为Groups[1]。

Regex类简述

先介绍了Match类再介绍Regex类是因为Regex的许多操作都和Match类相关,同时,Regex类是非常强大的,它提供了一些静态方法也是经常被使用的,比如Split方法,string类型的Split方法只能对char进行拆分,而Regex的Split方法则可以对任意字符串进行拆分。当然,每一个Regex的实例也包含了这些实用方法,静态方法只是简化了操作而已。

在新建Regex对象的时候,必须传入一个正则表达式,如果有额外要求,则需要传入一个RegexOptions的枚举类。比如我们在匹配的时候不区分大小写,因为不规范的HTML标签可能为大写,如果区分大小写,很可能查找失败。

Regex myRegex = new Regex("<li>([^<]*)</li>", RegexOptions.IgnoreCase);

使用RegexOptions可以帮助我们定制一些查询或替换模式,以下为其详细说明:

None 指定不设置选项。
IgnoreCase 指定不区分大小写的匹配。
Multiline 多行模式。更改 ^ 和 $ 的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。
ExplicitCapture 指定有效的捕获仅为形式为 (?...) 的显式命名或编号的组。这使未命名的圆括号可以充当非捕获组,并且不会使表达式的语法 (?:...) 显得笨拙。
Compiled 指定将正则表达式编译为程序集。这会产生更快的执行速度,但会增加启动时间。在调用 CompileToAssembly 方法时,不应将此值分配给 Options 属性。
Singleline 指定单行模式。更改点 (.) 的含义,使它与每一个字符匹配(而不是与除 \n 之外的每个字符匹配)。
IgnorePatternWhitespace 消除模式中的非转义空白并启用由 # 标记的注释。但是,IgnorePatternWhitespace 值不会影响或消除字符类中的空白。
RightToLeft 指定搜索从右向左而不是从左向右进行。
ECMAScript 为表达式启用符合 ECMAScript 的行为。该值只能与 IgnoreCase、Multiline 和 Compiled 值一起使用。该值与其他任何值一起使用均将导致异常。
CultureInvariant 指定忽略语言中的区域性差异。

常用的有IgnoreCase、Multiline、ExplicitCapture等

用Regex类实现正则表达式替换

下面谈谈常用的替换操作是怎样实现的,假设现在我们要修改最初提到的HTML文档,在每一个<li></li>标签中嵌入一个锚点链接<a>,并实现一个onclick的javascript方法,传入年份参数。具体如下:

<li>2006</li>
替换为
<li><a href="javascript:void(0);" onclick="showYearDetail('2006')">2006</a></li>

可以看到,2006这个年份参数是整个替换的“变量”,只要提取出了年份,通过在替换操作中反向引用即可。先来看看程序

string inputString = "<ul><li>2006</li><li>2007</li><li>2008</li><li>2009</li><li>2010</li></ul>";
Regex myRegex = new Regex("<li>([^<]*)</li>", RegexOptions.IgnoreCase);
if (myRegex.IsMatch(inputString))
{
    Match myMatch = myRegex.Match(inputString);
    while (myMatch.Success)
    {
        string year = myMatch.Groups[1].Value;//获取年份参数
         string replaceString = string.Format(
            "<li><a href=\"javascript:void(0);\" onclick=\"showYearDetail('{0}')\">{0}</a></li>", year);
        inputString = myRegex.Replace(inputString, replaceString, 1, myMatch.Index);//替换本次匹配
         myMatch = myMatch.NextMatch();//获取下次匹配
    }
}
Console.WriteLine(inputString);

以上程序重要的地方一个是变量year的获取,一个就是myRegex.Replace方法的使用,Replace方法共有6个重载,在应用中可以非常灵活。

另外,可以在Replace方法中传入带有反向引用的替换字符串达到同样的替换目的,而且能大大节省代码,如下

string inputString = "<ul><li>2006</li><li>2007</li><li>2008</li><li>2009</li><li>2010</li></ul>";
Regex myRegex = new Regex("<li>([^<]*)</li>", RegexOptions.IgnoreCase);
inputString = myRegex.Replace(inputString,
    "<li><a href=\"javascript:void(0);\" onclick=\"showYearDetail('$1')\">$1</a></li>");
Console.WriteLine(inputString);

可以看到,利用反向引用大大节省了代码长度,程序中的$1其实就是对年份变量的引用。

另外,如果在正则表达式中分组过多,可以使用将分组赋予别名,这样在反向引用的过程中可以直接利用别名来访问分组。在分组中定义别名,用格式(?<别名>******)即可,把上述程序改用别名得到的程序如下

string inputString = "<ul><li>2006</li><li>2007</li><li>2008</li><li>2009</li><li>2010</li></ul>";
Regex myRegex = new Regex("<li>(?<year>[^<]*)</li>", RegexOptions.IgnoreCase);
inputString = myRegex.Replace(inputString,
    "<li><a href=\"javascript:void(0);\" onclick=\"showYearDetail('${year}')\">${year}</a></li>");
Console.WriteLine(inputString);

总结

上述内容简单总结了使用C#正则表达式的一些技巧和方法,在实际应用过程中,定义正则表达式可能会很头痛,毕竟在复杂的应用中,定义匹配是很伤脑筋的事情。而且正则表达式具有非常不好的可读性,导致曾经写过的较长的pattern拿到现在来读也很困惑。但不管怎样,正则表达式为我们提供了处理字符串的有力工具,让我们用更短的时间实现更多的功能,十足是一件很棒的事情。当然,更多的东西还需要在实践中历练和体会。

Comments (2) -

shawb People's Republic of China
9/3/2010 4:05:23 PM #

这个介绍的很好,但是我有一个应用还不明白怎么实现。请教一下,谢谢:http://zhidao.baidu.com/question/180863829.html

Reply

iccna People's Republic of China
11/23/2010 7:10:53 PM #

谢谢,总结的很好,正好最近在研究爬虫,很有帮助。

Reply

(仅用于Gavatar)

  Country flag

biuquote
  • Comment
  • Preview
Loading

About

shinichi_wtnI'm Shinichi_wtn

Software Engineering Manager at Microsoft

[More...]

Widget Recent Tweets not found.

The remote server returned an error: (403) Forbidden.X


Month List