正则表达式简介
在许多应用场景中,我们需要在字符串中查找特定的信息,或者编辑其中的部分信息。比如网页信息采集程序需要对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拿到现在来读也很困惑。但不管怎样,正则表达式为我们提供了处理字符串的有力工具,让我们用更短的时间实现更多的功能,十足是一件很棒的事情。当然,更多的东西还需要在实践中历练和体会。