一般的正则替换,只能对匹配的子串做简单的处理,且只能是做 string 类型的处理之后,作为替换文本替换匹配子串,可以实现的功能比较有限。 .NET 的 Replace() 方法中的 replacement 参数,不仅可以是字符串,还可以是委托( delegate ),在每次匹配成功时,都会调用委托方法,对匹配的子串进行处理之后,再作为替换文本返回,匹配子串使用委托方法,可以做任意复杂的处理,因此这种替换功能非常强大。
委托的类型可以是 MatchEvaluator ,也可以是匿名方法,在每次匹配成功时调用。委托方法传入参数是 Match 对象,返回类型是 string ,即正则表达式在每次匹配成功时,会得到一个 Match 对象,作为参数传给委托方法,做一定处理后,返回替换文本,替换匹配到的子串。
在正则替换中使用的委托,一般有两种方式,显式声明的委托和匿名方法。下面以实例说明两种方式的使用方法。委托和匿名方法的区别和各自的特点不在这里介绍,请参考相关文献或文章。
举例 :
源字符串: a=10, b=20, c=30
需求:将字符串中的数字加 100 。
// 委托方法
private string regReplace(Match m)
{
return (Convert .ToInt32(m.Value) + 100).ToString();
}
// 声明一个MatchEvaluator 类型委托
MatchEvaluator me = new MatchEvaluator (regReplace);
// 正则替换应用
string test = "a=10, b=20, c=30" ;
Regex reg = new Regex (@"(?i)(?<=[a-z]=)/d+" );
string result = reg.Replace(test, me);
richTextBox2.Text = result;
/*-------- 输出--------
a=110, b=120, c=130
*/
事实上,对于以上这种简单的需求,不需要显式的声明委托,直接使用匿名方法即可,
string test = "a=10, b=20, c=30" ;
Regex reg = new Regex (@"(?i)(?<=[a-z]=)/d+" );
string result = reg.Replace(test, delegate (Match m) { return (Convert .ToInt32(m.Value) + 100).ToString(); });
richTextBox2.Text = result;
/*-------- 输出--------
a=110, b=120, c=130
*/
一个涉及到替换的需求,首先要进行分析,是否能够通过一个正则表达式进行直接替换,如果不可以,那就要借助委托了。接下来就要找出可在委托方法中进行处理的子串的规律,剩下的就是委托方法中最基本的字符串处理了。
正则中委托的典型应用场景一般可归纳为以下几种:
1 、替换子串需进行非 string 类型的处理,如计算等;
2 、替换子串需经过条件或逻辑判断来决定处理方式;
3 、多种条件组合的替换。
以上分类方式或许有重叠的地方,但是都比较有代表性,所以单独进行举例说明。
替换子串非 string 类型处理,最典型的就是以上举例中的计算。还有比较典型的就是涉及计数的问题。
举例 :
源字符串: <a href="http://www.sina.com.cn/"> 新浪 </a> <a href="http://www.sohu.com/"> 搜狐 </a> <a href="http://www.qq.com/"> 腾讯 QQ </a> <a href="http://www.163.com/"> 网易 163 </a>
需求:在每个链接后面加编号,结果
<a href="http://www.sina.com.cn/"> 新浪 </a>01 <a href="http://www.sohu.com/"> 搜狐 </a>02 <a href="http://www.qq.com/"> 腾讯 QQ </a>03 <a href="http://www.163.com/"> 网易 163 </a>04
代码实现:
string test = "<a href=/"http://www.sina.com.cn//"> 新浪</a><a href=/"http://www.sohu.com//"> 搜狐</a><a href=/"http://www.qq.com//"> 腾讯QQ</a><a href=/"http://www.163.com//"> 网易163</a>" ;
Regex reg = new Regex (@"(?is)<a[^>]*>(?:(?!</?a/b).)*</a>" );
int i = 1;
string result = reg.Replace(test, delegate (Match m) { return m.Value + (i++).ToString("00" ); });
richTextBox2.Text = result;
/*-------- 输出--------
<a href="http://www.sina.com.cn/"> 新浪</a>01<a href="http://www.sohu.com/"> 搜狐</a>02<a href="http://www.qq.com/"> 腾讯QQ</a>03<a href="http://www.163.com/"> 网易163</a>04
*/
这个需求是在链接后加编号,只要匹配到 <a…>…</a> 标签,在后面加上编号即可,但是由于编号是要根据 a 标签的个数来计数的,所以是动态变化的,这样直接替换就做不到了。而正则中的委托,是每次匹配成功后都会调用委托方法,而匹配是从左向右按顺序匹配的,所以调用委托方法也是按匹配的先后顺序进行调用的,这样就可以先用正则匹配出 a 标签,然后在委托方法中动态进行计数了。
如果待替换的子串,需要根据当前匹配子串的内容,经过判断后决定如何替换,一般无法直接通过 replace() 实现,需求在委托方法里进行判断。
举例 1 :
源字符串:源字符串规律为“字母 = 数字”,用“ & ”相连
a=12&b=34&c=56&d=78
a=98&b=76&d=54
需求:如果源字符串有“ c= 数字”,就替换为“ c=12 ”,否则在字符串结尾添加“ &c=98 ”。
代码实现:
string [] test = new string []{"a=12&b=34&c=56&d=78" , "a=98&b=76&d=54" };
Regex reg = new Regex (@"(?is)(?<=^(?:(?!c=).)*)(?(c=[^&]+)c=[^&]+|$)" );
foreach (string s in test)
{
richTextBox2.Text += " 字符串: " + s + "/n" ;
richTextBox2.Text += " 替换后: " + reg.Replace(s, delegate (Match m) { return m.Value == "" ? "&c=98" : "c=12" ; }) + "/n/n" ;
}
/*-------- 输出--------
字符串: a=12&b=34&c=56&d=78
替换后: a=12&b=34&c=12&d=78
字符串: a=98&b=76&d=54
替换后: a=98&b=76&d=54&c=98
*/
还有一个类似的需求实例。
举例 2 ( 一个可能很简单的正式表达式求助 ) :
源字符串:要处理的字符有可能是
"" (空)
"p=1"
"ID=e2798a59&xx=79d5&bb=4833-9c57&cc=87d46a8&bb=b907a"
"ID=e2798a59&xx=79d5&bb=4833-9c57&cc=87d46a8&bb=b907a&p=2"
"ID=e2798a59&xx=79d5&p=4&bb=4833-9c57&cc=87d46a8&bb=b907a"
需求:对上述任何一种字符串的可能,查找是否有 p=x ,如果找不到,为字符串加上 "p=0" ,如果找到,还要得到 x 的值,让 y=x+1 之后,再把 "p=y" 替换之前的 p=x 。
代码实现:
string [] test = new string [] { "" , "p=1" , "ID=e2798a59&xx=79d5&bb=4833-9c57&cc=87d46a8&bb=b907a" , "ID=e2798a59&xx=79d5&bb=4833-9c57&cc=87d46a8&bb=b907a&p=2" , "ID=e2798a59&xx=79d5&p=4&bb=4833-9c57&cc=87d46a8&bb=b907a" };
foreach (string s in test)
{
richTextBox2.Text += " 原始字符串: /t" + s + "/n" ;
richTextBox2.Text += " 替换后字符串: /t" + Regex .Replace(s, @"(?is)p=(?<v>/d+)|(?<!p=/d+.*)$" , delegate (Match m) { if (m.Groups["v" ].Value != "" ) return "p=" + (Convert .ToInt32(m.Groups["v" ].Value) + 1); return "p=0" ; }) + "/n/n" ;
}
/*-------- 输出--------
原始字符串:
替换后字符串: p=0
原始字符串: p=1
替换后字符串: p=2
原始字符串: ID=e2798a59&xx=79d5&bb=4833-9c57&cc=87d46a8&bb=b907a
替换后字符串: ID=e2798a59&xx=79d5&bb=4833-9c57&cc=87d46a8&bb=b907ap=0
原始字符串: ID=e2798a59&xx=79d5&bb=4833-9c57&cc=87d46a8&bb=b907a&p=2
替换后字符串: ID=e2798a59&xx=79d5&bb=4833-9c57&cc=87d46a8&bb=b907a&p=3
原始字符串: ID=e2798a59&xx=79d5&p=4&bb=4833-9c57&cc=87d46a8&bb=b907a
替换后字符串: ID=e2798a59&xx=79d5&p=5&bb=4833-9c57&cc=87d46a8&bb=b907a
*/
这个需求中,既涉及到了对替换子串的逻辑判断,又涉及到了数字运算,直接替换做不到,所以要考虑使用委托。先通过正则匹本出 p=x ,再在委托方法中进行逻辑判断和运算。
当需求中的条件多于一个时,可能无法在一个正则表达式中进行判断,或者即使能够在一个正则表达式中判断,由于正则表达式非常复杂,会降低匹配效率,所以还是要在委托方法中进行替换。
举例 1 :
源字符串: 第一个测试...<a href=/"www.test.com/"> 又一个测试</a>... 第三个测试...<a href=/"www.test.com/" title=/" 测试/" > 第几个测试了?</a>... 这是最后一个测试了...
需求:为字符串中的“测试”加链接,已有链接的不加。
这个需求,首先是要进行替换,但又加了一个附加条件,已有链接的不替换,这样如果在一个正则表达式中实现,正则太复杂,不但降低匹配效率,扩展起来也很困难,可读性也差,所以还是用正则委托来实现比较好。
先分析一下需求,在 <a…>…</a> 标签内的关键字不进行替换,那换个角度,只要先找出 a 标签外的字符串,对关键字进行替换就可以满足需求了。所以就是写正则,匹配出 a 标签外的子串,在委托方法中对关键字加链接,再替换回原字符串就可以了。
代码实现:
string test = " 第一个测试...<a href=/"www.test.com/"> 又一个测试</a>... 第三个测试...<a href=/"www.test.com/" title=/" 测试/" > 第几个测试了?</a>... 这是最后一个测试了..." ;
Regex reg = new Regex (@"(?is)^((?!</?a).)+|</a>((?!</?a).)+" );
string result = reg.Replace(test, delegate (Match m) { return m.Value.Replace(" 测试" , "<a href=/"www.test.com/"> 测试</a>" ); });
richTextBox2.Text = result;
/*-------- 输出--------
第一个<a href="www.test.com"> 测试</a>...<a href="www.test.com"> 又一个测试</a>... 第三个<a href="www.test.com"> 测试</a>...<a href="www.test.com" title=" 测试" > 第几个测试了?</a>... 这是最后一个<a href="www.test.com"> 测试</a> 了...
*/
当然,这个例子并不严谨,因为其它标签中也可能出现关键字,而这些关键通常也是不应该被替换的,这时也可以在委托方法中进行判断,以确定是否应该被替换。
举例 2 ( 正则去除不包含特定字符串的 A 标签 ~ ) :
源字符串: <a href=www.abc.com>abc </a> 啊啊啊 <a href=bcd.com>abc </a> 啊啊啊 <a href="www.abc.com" class="t1">abc </a> 啊啊啊 <a href=def.com>abc </a> 啊啊啊 <a href=efg.com>abc </a>
需求:把链接中不包含 “ abc ” 的超链接过滤掉。
这个需求,实际上也是两个条件,首先是要做替换,然后附加了一个条件,链接中不包含 “ abc ” 的替换。类似于这种符合某一规律的子串,部分替换,部分保留的情况,通常比较适合用正则委托来解决。
当然,这个需求还是可以直接通过一个正则表达式来处理的,先看一下这种处理方式的代码。
string test = "<a href=www.abc.com>abc </a> 啊啊啊 <a href=bcd.com>abc </a> 啊啊啊 <a href=/"www.abc.com/" class=/"t1/">abc </a> 啊啊啊 <a href=def.com>abc </a> 啊啊啊 <a href=efg.com>abc </a> " ;
Regex reg = new Regex (@"(?is)<a(?:(?!href=).)*href=(['""]?)(?:(?!abc|['""/s>]).)+/1(?:/s[^>]*)?>((?:(?!</?a/b).)*)</a>" );
string result = reg.Replace(test, "$2" );
richTextBox2.Text = result;
/*-------- 输出--------
<a href=www.abc.com>abc </a> 啊啊啊 abc 啊啊啊 <a href="www.abc.com" class="t1">abc </a> 啊啊啊 abc 啊啊啊 abc
*/
可以看到,这种处理方式,是先进行判断,再进行匹配。在正则表达式中,对链接子串的每一个字符用“(?!abc|['""/s>]). ”进行判断,所以有多少个字符,就要判断多少次,在这种情况下,通常需要使用“| ”来对不同的条件取“或”,而“| ”的效率一般是比较低的。
另一种处理方式,是先把链接匹配出来,然后在委托方法中进行判断,以决定是否替换。
代码实现:
string test = "<a href=www.abc.com>abc </a> 啊啊啊 <a href=bcd.com>abc </a> 啊啊啊 <a href=/"www.abc.com/" class=/"t1/">abc </a> 啊啊啊 <a href=def.com>abc </a> 啊啊啊 <a href=efg.com>abc </a> " ;
Regex reg = new Regex (@"(?is)<a(?:(?!href=).)*href=(['""]?)([^'""/s>]+)/1[^>]*>((?:(?!</?a/b).)*)</a>" );
string result = reg.Replace(test, delegate (Match m) { if (m.Groups[2].Value.IndexOf("abc" ) > -1) return m.Value; return m.Groups[3].Value; });
richTextBox2.Text = result;
/*-------- 输出--------
<a href=www.abc.com>abc </a> 啊啊啊 abc 啊啊啊 <a href="www.abc.com" class="t1">abc </a> 啊啊啊 abc 啊啊啊 abc
*/
这种处理方式,是先进行匹配,再进行判断。先通过正则把每一个链接都匹配出来,作为参数传给委托方法,在委托方法中判断是否包含“ abc ”,以决定是否替换。这种方式因为匹配过程中不需要进行判断,所以匹配的速度是很快的,然后在委托方法中只执行一次判断即可。两种处理方式的效率,在字符较少时区别不大,在字符较多,调用较频繁的情况下,还是委托方法的效率比较高。
类似于这种需求,在效率、可读性、可扩展性等方面综合考虑,还是使用委托方法要好一些。