Single-character tag matching
我们通常会碰到对字符串中的被tag包围的内容进行匹配的情况,比如
abc "Hello, world"
我们需要匹配""中的字符串,那么可以使用如下的regex
"[^"]*"
这里我们使用贪婪匹配来匹配字符串,为了防止过度匹配的情况发生,我们使用了排除型字符组,即[^"]来匹配引号中的内容。这里我们还可以用忽略优先来匹配
".*?"
因为忽略优先的特性,所以不会出现过度匹配的情况,即不会匹配
abc "hello, world" "hello, boy"
Multi-characters tag matching
当我们要匹配不是简单的引号中的内容时,即如果一个tag包含多个字符,这种情况,使用简单的排除型字符组就不奏效了。比如HTML/XML中,所有element都是多字符的。
<b>hello</b>
这里,如果我们简单地用
<b>[^</b>]*</b>
是不行的,因为这里[^</b>]不是说不匹配</b>这个tag,而是不匹配<,/,b,>这些字符,所以以下合法的内容将不能匹配
<b>/abc</b>
这里我们需要检查的是,当一个字符不是</b>时,才会去匹配,也就是跟单字符tag匹配时一样,这次需要排除</b>而已。我们可以用Lookaround来达到这个目的。
<b>((?!</b>).)*</b> (1)
Lookaround会迫使正则引擎检查当前位置是否是</b>,如果是,则不匹配,于是会尝试匹配</b>。这里我们需要注意,不能写成
<b>(.(?!</b>))*</b>
这个表达式意义同上面那个不一样,这里会先匹配任意字符,之后匹配后,检查当前位置是否是</b>。
同单字符tag匹配一样,我们也可以使用忽略优先来匹配:
<b>.*?</b> (2)
这里不需要排除</b>,理由同单字符tag匹配时一样。
Handling Nested Tags
上面的方案无法解决有嵌套tag的情况,比如
<b>hello<b>world</b>
如果采用(1),那么很显然,我们这里会匹配整个字符串,但是<b>hello通常不是我们的期望匹配。而(2)同样也无法解决,还是会过度匹配。
这里其实我们需要匹配的是除了所有starting tag,和ending tag之外的字符,也就是说我们需要
start(except start, end)*end
对于贪婪匹配,我们必须同时排除starting和ending tag,
- 排除starting tag是为了防止匹配嵌套的tag
- 排除ending tag是为了防止过度匹配. e.g. <b>blahblah</b>foo</b>
<b>((?!<b>|</b>).)*</b> => <b>((?!</?b>).)*</b>
对于非贪婪匹配,我们只需排除starting tag即可,因为我们总是不会过度匹配</b>
<b>((?!<b>).)*?</b>
Optimization
匹配时我们已经可以使用忽略优先来匹配tags了,但是对于某些tag,我们还可以做得更好,比如C comment。C的注释形如/*...*/,其中/**/不能嵌套,我们当然可以用忽略优先来构造,得到:/\*((?!/\*).)*?\*/
但其实,我们也可以更直接,使用:
/\*.*?\*/
这里能够这样简单地使用忽略优先来完成匹配的原因是:/**/不能嵌套,这就保证了,里面不会出现/*,所以我们可以不用排除/*。
用python做性能测试结果表明,第二个表达式的效率比第一个表达式提升约69%。
Conclusion
对于tag匹配的问题,我们得到了两种解决方案
- 使用贪婪匹配
start((?!start|end).)*end - 使用非贪婪匹配,且start和end之间可能存在嵌套
start((?!start).)*?end - 使用非贪婪匹配,且start和end之间不会存在嵌套
start.*?end
对于single-character tag,我们只需用简单的排除型字符组替换Lookaround即可,得到
- 使用贪婪匹配
s[^se]*e - 使用非贪婪匹配
s[^s]*?e
Note
Python中的正则表达式不支持在Lookaround中使用非固定长度的表达式,需要注意tag的构造。
Reference
[1] Mastering Regular Expression