PHP正则匹配不出现某段字符串

网友cfc4n问及关于(?!)的正则表达式问题。回答之后,顺便总结了一下Perl语言中如何匹配“不出现”某元素,贴在这里。

问题

问题描述

有如下文本,如何使用正则式,将其中不含color选项的item匹配出来?

<item>
color:red;
</item>
<item>
size:12;
number:45;
type:good;
</item>

典型的错误答案

新手容易提供这样的错误答案:<item>.*?(?!color).*?</item>。其出发点是正确的:只有当color不出现在目标字串时,该匹配才是所需要的。事实上,这样的正则表达式不能如君所愿,它匹配所有的<item>...</item>。这是为什么呢?

排除型匹配

不好意思,“排除型匹配”这个词是我生造的。其它的说法或许是“否定断言”,“否定环视”等等。后两者的命名,都是从匹配过程的角度出发;而此处命名,是从结果出发。具体说来,就是使用 (?!...)(?<!...)作为辅助条件判断,来简化正则表达式,方便快捷地找到符合要求的匹配。

这两个东东的使用方法类似,都是指,当前位置不出现某种模式。不同的是,(?!...)是指当前位置的右边,而(?<!)自然就是指左边了。

这里隆重推出Anrs同学翻译的教程: 环视一以及环视二。仔细阅读这两文章,彻底明白环视这两个概念,将会提升您的正则表达式功力。后文将建立在您已经理解环视这个概念的基础上。

闲话一句。既然使用“左边”和“右边”既形象又好懂,为什么没见过“左瞻”,“右瞻”,“左向”,“右向”,反而全是些“前瞻后瞻”,“正向逆向”这样的不好理解的说法呢?撕烤者也同有此问。我的理解是,或许是为了照顾阿语等从右向左书写的用户的习惯吧。无论如何,将从 ^$的方向称之为“向前”总不会错。

描述当前位置(左侧或右侧)的模式,从而辅助判断正则式是否匹配,是环视的作用。它只描述,不消耗字符;只辅助判断,从不单独出现。这与^$简直如出一辙。

一则例子

例子. 现在有许多与fanfou.com类似的网址。如何写一条正则表达式,来匹配域名含fanfou,但是TLS不是.com的模式?

答案:/\bfanfou\.(?!com)[a-z]{2,4}\b/i。分析这条正则表达式:

  • \b开始,明确字符边界;
  • fanfou主域名不可少;
  • \.匹配一个普通的点号;此处不要使用点号元字符;
  • (?!com)表示此处(即从fanfou.的右边)不得出现com三个连续字符;
  • [a-z]{2,4}表示是2至4位的拉丁字母;因为域名的TLS最短是2位(如.au, .us),最长可为4位(如.info, .asia);
  • 右侧边界同样重要,否则我们之前的{2,4}就白费了;
  • 使用i表示不分大小写;这是域名的特征之一。

回到本题

按照要求,一步步建立这条正则式。

  • 该正则式匹配的是<item>...</item>结构。因此,正则式以<item>开始。
  • <item></item>之间不得出现color,是这条正则式的难点。因为,color可能位于这个结构之内的任意一点,因此要规定,此内任意一点都不得出现color一词。这样的点为:(?!color).。这样的点重复1+次,正则式写为((?!color).)+。注意这里有个小陷阱:不要写为(?!color).+,否则它只描述了最左侧的一点不得出现color,其余部分则都无所谓。而写为((?!color).)+则保证每一点都不出现color。
  • 正则式此时为<item>((?!color).)+?</item>。为了节省资源,括号通常写成非捕获模式(?:...);为了保证点号匹配换行符,可以指定s模式或使用[\s\S]代替点号元字符。此处仍使用点号。正则式修改为<item>(?:(?!color).)+?</item>

总体来说,环视相对于基本的元字符还是要抽象一些。不过一旦理解并掌握了它,就会发现它在精确匹配和替换时十分有用。上面的分析,希望有所帮助。如果您有类似的问题,欢迎提出。

 

原文来自:http://iregex.org/blog/negate-match.html

发表评论

邮箱地址不会被公开。 必填项已用*标注