[转]高级正则表达式背后的关键概念

高级正则表达式背后的关键概念

By Karthik Viswanathan 翻译:Wind

原文参考:http://www.smashingmagazine.com/2009/05/06/introduction-to-advanced-regular-expressions/
2009-5-6

(Wind:和师兄探讨RegEx有一小段时间了,自己去翻资料看到这篇文章,觉得很好,就自己去翻译了一下,虽然自己英文水平有限,还是努力地去尝试,呵呵~有需要的可以慢慢看。^_^)

正则表达式(RegEx)是处理字符串获取信息的强大手段。它们通过一种字符串构造成一些匹配模式,然后展现它神奇的魅力。但不幸的是,简单的正则表达式并不满足于处理复杂的模式和符号。为了解决这个困境,你可以使用一些高级的正则。

接下来,我们将给您带来高级正则的简单介绍,一共有八个常用的概念和例子。每一个举例都描绘了匹配复杂字符串模式的简单方式。如果你之前没有什么使用正则表达式的经验,请查阅相关的资料^_^

1.贪婪模式/懒人模式(Greediness/Laziness)
所有的正则操作符都是贪婪的。它们尽可能多地匹配字符串。但这样你经常会得不到期望的结果。因此,我们有懒人模式操作符来解决这个问题。区分贪婪模式与懒人模式是完全理解高级正则表达式的关键。

贪婪模式操作符
*(星号)操作符匹配它之前的表达式0次或多次。这是一个贪婪模式的操作符。考虑下面的表达式:
preg_match( '/<h1>.*<\/h1>/', '<h1>This is a heading.</h1>
<h1>This is another one.</h1>', $matches );

请记住那个.(点)是代表除新行外的任何符号。上面的正则表达式就是查找一个h1标签以有它的所有内容。它使用.和*运算符来匹配标签内的所有内容。这个模式将匹配到:
<h1>This is a heading.</h1><h1>This is another one.</h1>

它会返回整个字符串,*操作符会持续地匹配所有内容——即使中间有h1的关闭标签——因为它是贪婪的。它会匹配它所能匹配的。

懒人模式操作符
让我们通过加上?(问号)来改变上面的操作符吧,这将使上面的表达式变成懒人模式。
/<h1>.*?<\/h1>/

这个正则现在是只匹配第一个h1标签来完成任务。另一个贪婪模式的操作符可以达到同样的效果,就是{n,},这将匹配它前面表达式N次或者更多次,如果不伴随着一个问号使用,它将查找尽可能多的重复。另外要注意的是,它用第N次重复开始。
# Set up a String
$str = 'hihi';

# Match it using the greedy {n,} operator
preg_match( '/(hi){1,}/', $str, $matches ); # matches[0] will be 'hihi'

# Match it with the lazy {n,}? operator
preg_match( '/(hi){1,}?/', $str, $matches ); # matches[0] will be 'hi'


2.反向引用(Back Referencing)
它能干什么?
反向引用是一种用于引用正则表达式里之前匹配到的模式的途径。打个比方,先看看下面的简单正则,它匹配的是引号里的表达式:
# Set up an array of matches
$matches = array();

# Create a String
$str = "\"This is a 'string'\"";

# Traverse it with regular expressions
preg_match( "/(\"|').*?(\"|')/", $str, $matches );

# Print the whole match
echo  $matches[0];
很不幸,这个正则并不会正确地匹配字符串,取而代之,它会输出:
"This is a '

这个正则表达式匹配了开双引号,但同时找了不同类型的引号来关闭。这是因为它在结尾的时候同时提供了单引或双引的选择。为了修正这个问题,你可以使用反向引用。表达式\1,\2,...,\9用来引用已获取的子模式,在这个例子中,变量\1就保存着第一个匹配的引号。

怎样去使用?
为了将这种概念应用于前面的例子,我们在最后一个引号处使用\1:
preg_match( '/("|\').*?\1/', $str, $matches );

现在,这将正确地返回:
"This is a 'string'"

记住,反向引用也可以被用于preg_replace。同时注意相对于\1 ... \9,你应该使用$1 ... $9 ... $n(这里所有的数字都可以)。例如,假如你想用所有代表段落标签的文本替换掉它们,就用:
$text = preg_replace( '/<p>(.*?)<\/p>/',
"&lt;p&gt;$1&lt;/p&gt;", $html );

这里$1反向引用保存了段落里的文本,它在自己的替换模式中使用。这是完全合法的表达式,它展现了一种简单的方式在替找的时候来使用匹配的模式。

3.命名组(Named Groups)
当要使用多个反向引用时,正则表达式很快就会变得令人迷惑而且很难去理解。除使用反向引用的另一种选择是使用命名组。一个命名组是通过(?P<name>pattern)来指定的,name就是组的名字,pattern就是组中的正则表达式。这个命名组可以通过(?P=name)的方式引用。例如,考虑如下表达式:
/(?P<quote>"|').*?(?P=quote)/

上面的表达式将产生和前面的反向引用同样的效果,但通过使用命名组,这将明显地更便于阅读。
命名组在匹配数组的筛选中同样有用。给出的特定模式的名字也是匹配数组相应的键。
preg_match( '/(?P<quote>"|\')/', "'String'", $matches );

# This will print "'"
echo $matches[1];

# This will also print "'", as it is a named group
echo $matches['quote'];

4.单词边界(Word Boundaries)
单词边界是单词字符与非单词字符之间的地方。这些边界的特征就是,它们现实中并不匹配字符。它们的长度为0。表达式\b匹配任何的单词边界。

但是,由于很多人都没有认识到它的意义所在,所以边界经常会被忽略。比方说你想匹配一个单词import
/import/

注意了!正则表达式具有欺骗性。上面的表达式同样可以匹配important
你觉得这很简单,只需在单词的前后加上空格就可以防止这种错误的匹配:
/ import /
但是,有没想过这种情况?
The trader voted for the import
当import是在一个字符串的开头或者结尾,那么修改后的正则表达式会失败,这就需要分情况考虑了:
/(^import | import | import$)/i
回过头来看看我们的正则表达式,它没有考虑问号与其他标点,仅仅为了匹配一个单词,正则表达式就有可能要这样写:
/(^import(:|;|,)? | import(:|;|,)? | import(\.|\?|!)?$)/i
对于匹配一个单词来讲,这代码太多了。这就是为什么单词边界意义如此重大。要完成上面的表达式,使用单词边界的话会有很多的变式,所有必须做的仅是:
/\bimport\b/

这将匹配上面的所有情况。\b的灵活性源自于它匹配0长度的字符串。它所匹配的是想像出来的字符间的空隙。它检查单词字符的下一个字符是否为非单词字符。如果是的话,就匹配它。若是遇到了字符串的开头或结尾,\b将它当成一个非单词字符。由于i在import里依然是被当成是单词字符,所以它将匹配import。

注意,与\b相反的是\B,这个操作符将匹配两个单词字符、或两个非单词字符之间的空隙。因而,如果你想匹配其他单词里的"hi",你可以使用\Bhi\B

5.原子组(Atomic Groups)
原子组是正则的群组里特殊的一种,它不被捕获。它们通常都是被用于提高正则表达式的效率,但也可能是应用于排除某些匹配。一个原子组可以用(?>pattern)来界定:
/(?>his|this)/
当正则表达式的引擎匹配一个原子组时,它将忽略包含里面所有记号的回溯。比如说单词"smashing",使用上面的正则,正则的引擎首先会尝试在"smashing"匹配"his"。这个不能匹配,此时,原子组就产生效果了。引擎会忽略所有的回溯位置。这就意味着它不会再从"smashing"里查找"this"。为什么呢?如果"his"都没法返回匹配的串,那很明显"this"(包含了"his")也不会有确定的结果。

上面的例子并没多少实际的用途,我们同样可以使用/t?his?/来代替。请看下面的例子:

/\b(engineer|engrave|end)\b/
如果正则引擎得到单词"engineering",它将正确地匹配"engineer"。但第二个单词边界\b就无法匹配。因此,它将转向下一个匹配:engrave。它知道"eng"可以匹配,但剩余的则不能。最后,"end"也尝试了,同样失败。如果你看得仔细,你会看到一旦引擎匹配了"engineer"而且没有匹配成功单词边界,它就很可能不能匹配"engrave"或者"end"。这两个匹配都比"engineer"字母少,因此正则引擎不该继续它们的尝试。

/\b(?>engineer|engrave|end)\b/
上面的例子就成了更好的选择,它将节省正则引擎的时间并提高代码的效率。

6.递归(Recursion)
递归在正则表达式里可以用来匹配嵌套结构,例如括号,(this(that))、还有HTML标签,<div></div>。它们需要用到(?R),一个匹配循环子模式的操作符。考虑一下正则用来匹配嵌套括号的情况:
/\(((?>[^()]+)|(?R))*\)/
这个正则里最外层的括号匹配了嵌套结构的开头。然后一个选择操作符,表明可以匹配非括号的字符(?>[^()]+)或者将整个表达式作为一个子模式再匹配一次,(?R)。注意这个操作符会一直重复下去直到匹配所有的嵌套的括号。

另一个使用递归的情况如下:
 /<([\w]+).*?>((?>[^<>]+)|((?R)))*<\/\1>/

上面的例子结合了字符组,贪婪操作符,反引以及原子组,用来匹配嵌套的标签。第一个括起来的组([\w]+)匹配标签名,它将在后面的表达式中用到,然后开始匹配标签剩余的部分。第二个括起来的子表达式和之前的例子相似。它既可以匹配非标签(?>[^<>]+)字符,又可以重新匹配另一个标签(?R)。最终,最后一部分表达式匹配关闭标签。

7.调用(Callbacks)
某些模式的匹配需要特殊的修改。要完成多个或者复杂的修改,可以使用调用。调用是用于在preg_replace_callback函数里动态地替换字符串的。在匹配的时候,它们将函数作为一个参数的形式传入。那个函数则接收匹配到的数组作为参数,然后返回一个修改后的字符串作为替换。

例如,要将所有的单词改成大写,但PHP没有正则的操作符来改变字符的大小写。要完成这个任务,我们应该使用调用。首先,表达式必须匹配所有需要大写的字母。
/\b\w/
上面的表达式用到了单词边界和字符类。我们有了这个表达式之后,我们就可以写调用函数了:
function upper_case( $matches ) { 
    return strtoupper( $matches[0] ); 
}
upper_case传入一个匹配的数组,然后返回被匹配模式的大写模式。$matches[0]在这里代表了需要被大写的字母。这些现在可以都使用preg_replace_callback函数来结合到一起:
preg_replace_callback('/\b\w/', "upper_case", $str);
这就是简单的调用的力量。

8.注释(Commenting)
注释实际上并不匹配字符串,但它是正则表达式中最重要的一部分。随着你陷入越来越庞大、越来越复杂的表达式中,这将变得越来越难去理解究竟真正会匹配到什么。使用注释是使这些困惑最小化的完美方法。

要在正则表达式中加入注释,使用(?#comment)的格式,"comment"可以换成任何你需要的注释文字。
/(?#digit)\d/

在你要公开发布的正则表达式里加入注释尤为重要。你的正则的使用者可以方便地明白和修改以达到他们的需求。它同样可以帮助你在重读程序的时候方便解码。

如果用到"x"或者(?x)修饰符来使用注释的自由空格模式,这就可以让正则表达式忽略字符间的空格,所有的空格都代表[ ]或者\ (一个反斜杠加一个空格)。
/
\d #digit
[ ] #space
\w+ #word
/x
下面的和上面是一样的:
/\d(?#digit)[ ](?#space)\w+(?#word)/

(Always create well-documented code.)

posted @ 2011-04-21 08:52  miuq  阅读(1117)  评论(0编辑  收藏  举报