最近在工作中遇到一个需求,就是找出html中所有锚文字包含 联系方式 的超链接。刚开始我写了一个很简单的正则来解决这个问题<a.*?联系方式.*?</a
。但是在测试的时候却发现这个正则表达式并不像我想象的那样工作。
图中给出了一个正则表达式匹配的例子,可以看出在这段文字中有两个匹配,但是第一个匹配所包含的结果已经超出了实际需要的范围,包含了太多的超链接标签,而我需要的是最短的匹配也就是图中横线画出的范围,这是怎么回事?
正则匹配的原理
这要从正则匹配的原理说起,简单的来说正则匹配是一种贪心的算法。它总是先找到第一个匹配的位置,然后向后继续匹配其他的表达式符号。对于本文给出的正则表达式,会现在html中找到一个<a
标签,然后之后是.*?
,直到找到一个联系方式
,在这个过程中如果找到了另一个<a
,会被当作.*?
匹配的部分,而忘记了要匹配表达式的开头就是<a
。也就是说,除非发生失配,正则表达式不会主动地回溯。尽管使用了?
来表达非贪婪匹配,也只能限制向后匹配时尽可能地短,而不能缩短已匹配部分的长度。也就是说,非贪婪匹配向后是最短匹配,但是向前不是最短匹配。
对于这个任务,我后来使用了其他效率更高的方法实现了,但是有没有可能使用正则表达式来完成这个任务呢?
零宽断言
零宽断言是一种零宽度的匹配,它匹配到的内容不会保存到匹配结果中去,最终匹配结果只是一个位置而已。
作用是给指定位置添加一个限定条件,用来规定此位置之前或者之后的字符必须满足限定条件才能使正则中的字表达式匹配成功。
零宽断言总共有四种
对于这个需求,实际上应该找到离联系方式
最近的一个<a
,也就是说,在<a
到联系方式
之前不能再有其他的<a
了。而最开始的正则匹配表达式<a.*?联系方式.*?</a
中的.*?
可以通过.
来匹配任意一个字符,在这里可以使用零宽度负先行断言来限制.
匹配的任何一个字符的右侧不能够再有<a
。也就是.(?!<a)
,再将这个整体重复多次(.(?!<a))*?
。这里引入了一个额外的括号,为了不产生多余的匹配,可以使用非捕获组来去除不需要的匹配,最终可以将整个表达式写成<a(:?.(?!<a))*?联系方式.*?</a
。
可以看到匹配的范围已经缩小到最后一个出现的超链接。
总结
因为正则表达式实现原理的限制,尽管选择非贪婪匹配,匹配到的结果也不一定是最短的匹配。
通常正则表达式总是表明了“要匹配什么”,而通过零宽度负断言,则可以表明“不匹配什么”,这比字符集中使用^
来取反更加强大。