4.10 Perl兼容正则表达式
Perl-Compatible Regular Expressions
一直以来,Perl被视为强大的正则表达式的标准。PHP使用一个被称为pcre的C库,几乎完全支持Perl正则表达式的特性。Perl正则表达式包括之前介绍过的POSIX类和锚。在Perl正则表达式中,POSIX风格的字符类可以操作和理解使用Unix地区系统的非英语字符。Perl正则表达式可以作用于任意的二进制数据,所以你可以安全地对带有空字节(NUL-byte,\x00)的模式或字符串进行匹配。
4.10.1 分隔符
Delimiters
Perl风格的正则表达式模仿Perl模式的语法,即每个模式都必须用一对分隔符括起来。习惯上使用左斜杠(/),例如,/pattern/。不过,任意非数字字母的字符(除了反斜杠(\))都可用于分隔一个Perl风格的模式。这在匹配包含斜杠的字符串是很有用的,如文件名。例如,下面的语句是等效的:
preg_match('/\/usr\/local\//', '/usr/local/bin/perl'); //返回true
preg_match('#/usr/local/#', '/usr/local/bin/perl'); // 返回true
小括号(())、大括号({})、中括号([])和尖括号(<>)可被作为模式分隔符使用:
preg_match('{/usr/local/}', '/usr/local/bin/perl'); //返回true
“后缀选项”一节中将讨论放在结束分隔符后面的单个字符修饰符,它用于修改正则表达式引擎的行为。非常有用的一个是x,它可以让正则表达式引擎在匹配前从正则表达式中跳过空白符和被#标记的注释。下面这两种模式是相同的,但是其中一个会更易于阅读:
'/([[:alpha:]]+)\s+\1/'
'/( # start capture
[[:alpha:]]+ # a word
\s+ # whitespace
\1 # the same word again
) # end capture
/x'
4.10.2 匹配行为
Match Behavior
虽然Perl正则表达式语法包含我们之前谈到的POSIX结构,但在Perl中一些模式组件有不同的意义。Perl正则表达式特别为单行文字匹配进行了优化(虽然有一些选项来改变这个行为)。
句点(.)匹配任意除换行符(\n)之外的字符。美元符号($)匹配字符串的末尾或在换行符之前以换行符结尾的字符串。
preg_match('/is (.*)$/', "the key is in my pants", $captured);
// $captured[1] 是 'in my pants'
4.10.3 字符类
Character Classes
Perl风格的正则表达式不仅支持POSIX字符类,而且定义了一些自己的字符类,如表4-9所示:
表4-9 Perl风格的字符类
|
字符类 |
意 义 |
扩 展 |
|
\s |
空白符 |
[\r\n \t] |
|
\S |
非空白符 |
[^\r\n \t] |
|
\w |
单词(标识符)字符 |
[0-9A-Za-z_] |
|
\W |
非单词(标识符)字符 |
[^0-9A-Za-z_] |
|
\d |
数字 |
[0-9] |
|
\D |
非数字 |
[^0-9] |
4.10.4 锚
Anchors
Perl风格的正则表达式也支持附加锚(其自定义的锚标记),如表4-10所示:
表4-10:Perl风格的锚
|
断 言 |
意 义 |
|
\b |
单词边界(在\w和\W之间或在字符串开头或末尾) |
|
\B |
非单词边界(在\w和\w或\W和\W之间) |
|
\A |
字符串开头 |
|
\Z |
字符串末尾或在末尾的\n之前 |
|
\z |
字符串末尾 |
续表4-10:Perl风格的锚
|
断 言 |
意 义 |
|
^ |
行的开头(或如果/m标志启用的话在\n之后) |
|
$ |
行的末尾(或如果/m标志启用的话在\n之前) |
4.10.5 量词和贪婪性
Quantifiers and Greed
Perl也支持POSIX量词,而且是具有贪婪性的(greedy)。即当有一个量词时,引擎在仍然满足匹配模式的情况下尽可能多地进行匹配。例如:
preg_match('/(<.*>)/', 'do <b>not</b> press the button', $match);
// $match[1] 为 '<b>not</b>'
(这里“<b>”、“</b>”和“<b>not</b>”都满足模式“/(<.*>)/”,根据贪婪性原则,取最大的部分,所以$match[1] 为“<b>not</b>”。――译者注)
这个正则表达式从第一个小于符号开始匹配到最后一个大于符号。“.*”匹配在第一个小于符号之后的所有字符,并且引擎回溯使得它的匹配越来越少,直到匹配到最后一个大于符号。
贪婪性会产生一个问题。有时你需要最少匹配(非贪婪匹配),就是说量词尽可能少的匹配满足模式的剩余部分。Perl提供了一组用于最小匹配的量词。它们很容易记住,因为它们和贪婪量词相同,只是附加了一个问号(?)。表4-11显示了Perl风格正则表达式中相应的贪婪量词和非贪婪量词。
表4-11:Perl兼容正则表达式中支持的贪婪量词和非贪婪量词
|
贪婪量词 |
非贪婪量词 |
|
? |
?? |
|
* |
*? |
|
+ |
+? |
|
{m} |
{m}? |
|
{m,} |
{m,}? |
|
{m,n} |
{m,n}? |
下面是如何使用一个非贪婪量词来匹配标签:
preg_match('/(<.*?>)/', 'do <b>not</b> press the button', $match);
// $match[1] 是 '<b>'
另外一个更快的方法是使用一个字符类来匹配每个非大于字符到下一个大于字符:
preg_match('/(<[^>]*>)/', 'do <b>not</b> press the button', $match);
// $match[1] 是 '<b>'
4.10.6 非捕获匹配
Non-Capturing Groups
如果把模式的一部分用小括号括起来,那么匹配子模式的文本被捕获并且可以在后面访问。但是有时你想创建一个不捕获匹配文本的子模式,那么在Perl兼容正则表达式中可以使用(?:subpattern)结构来这样做:
preg_match('/(?:ello)(.*)/', 'jello biafra', $match);
// $match[1] 是 ' biafra'
4.10.7 逆向引用
Backreferences
可以使用一个逆向引用(backreference)来引用模式中之前被捕获的字符串:\1引用第一个子模式的内容,\2引用第二个,依此类推。如果嵌套了子模式,那么第一个引用以第一个左小括号开始,第二个引用以第二个左小括号开始,依此类推。
例如,下面识别两倍的单词:
preg_match('/([[:alpha:]]+)\s+\1/', 'Paris in the the spring', $m);
// returns true and $m[1] is 'the' 返回true并且$m[1]是’the’
不能捕获超过99个子模式。
4.10.8 后缀选项
Trailing Options
Perl风格的正则表达式允许把单个字符选项(标志)放在正则表达式模式后面来修改匹配的解释或行为。例如,要进行不区分大小写的匹配,可以简单地使用i标志:
preg_match('/cat/i', 'Stop, Catherine!'); // returns true返回true
表4-12显示了在Perl兼容正则表达式中支持的来自Perl的修饰符:
表4-12:Perl标志
|
修饰符 |
意 义 |
|
/regexp/i |
不区分大小写的匹配 |
|
/regexp/s |
使句点(.)匹配任何字符,包括换行符(\n) |
|
/regexp/x |
从模式中删除空白符和注释 |
|
/regexp/m |
使^匹配换行符 (\n)之后的内容,美元符号($)匹配换行符 (\n)之前的内容 |
|
/regexp/e |
如果替换字符串是PHP代码,使用eval()执行该代码来得到实际的替换字符串。 |
PHP的Perl兼容正则表达式函数也支持在Perl中不支持的其他修饰符,如表4-13所示:
表4-13:其他的PHP标志
|
修饰符 |
意 义 |
|
/regexp/U |
颠倒子模式的贪婪性;*和+尽可能少地匹配而不是尽可能多。 |
|
/regexp/u |
把模式字符串当作UTF-8编码对待 |
|
/regexp/X |
如果一个反斜杠之后跟着没有特殊意义的字符,将产生一个错误 |
|
/regexp/A |
把锚定位在字符串的开头就像模式中有^一样 |
|
/regexp/D |
使$字符仅匹配一行的末尾 |
|
/regexp/S |
使表达式解析器更加小心地检查模式的结构,使得第二次运行时(如在一个循环中)加快速度 |
在一个模式中可以使用多个选项,如下所示:
$message = <<< END
To: you@youcorp
From: me@mecorp
Subject: pay up
Pay me or else!
END;
preg_match('/^subject: (.*)/im', $message, $match);
// $match[1] 是 'pay up'
4.10.9 内联选项
Inline Options
除了在模式结束分隔符之后指定模式选项之外,还可以在一个模式内部指定仅运用于部分模式的选项。语法如下:
(?flags:subpattern)
例如,在这个示例中只有单词"PHP"是不区分大小写的:
preg_match('/I like (?i:PHP)/', 'I like pHp'); //返回true
i、m、s、U、x和X选项可被用在这种方式的内部。一次可以使用多个选项:
preg_match('/eat (?ix:fo o d)/', 'eat FoOD'); //返回true
Prefix an option with a hyphen (-) to turn it off: 一个选项前如果有连字符(-)表示关闭此选项:
preg_match('/(?-i:I like) PHP/i', 'I like pHp'); //返回true
可以启用或禁用标志直到封闭的子模式或模式末尾:
preg_match('/I like (?i)PHP/', 'I like pHp'); // returns true 返回true
preg_match('/I (like (?i)PHP) a lot/', 'I like pHp a lot', $match);
// $match[1] 是 'like pHp'
内置标志不能用于捕获字符串,需要设置一个附加的小括号来完成捕获(如上示例中用小括号定义了一个子模式)。
4.10.10 前向和后向断言
Lookahead and Lookbehind
有时在模式中能够指出”如果这里是下一个,就匹配这里”是很有用的。这在拆分字符串时是很常见的。这种正则表达式描述了分隔符,但不返回。可以使用前向断言(lookahead assertion)来确保在分隔符之后有更多的数据(因为没有匹配它,所以阻止被返回)。类似的,后向断言(lookbehind assertion)检查前面的文字。
前向和后向各有两种形式:正(positive)和负(negative)。正的前向或后向表示“下一个/前面的文本必须如此”。负的前向和后向表示“下一个/前面的文本必须不是这样”。表4-14展示了在Perl兼容模式中可以使用的4种结构。这4种结构都不捕获文本。
表4-14:前向和后向断言
|
结 构 |
意 义 |
|
(?=subpattern) |
正前向 |
|
(?!subpattern) |
负前向 |
|
(?<=subpattern) |
正后向 |
|
(?<!subpattern) |
负后向 |
正前向的一个简单应用是将一个Unix mbox邮件文件分解成单独的消息。单词"From"通过自己开始一行来指出一条新消息的开始,所以可以通过在一行开始处指定分隔符作为下一个文本中"From"的位置来把mailbox拆分为消息:
$messages = preg_split('/(?=^From )/m', $mailbox);
负后向的一个简单应用是析取包含引用分隔符的引用字符串。例如,下面的例子是告诉你如何析取一个用单引号括起来的字符串(注意正则表达式使用x修饰符,可以对模式加上注释):
$input = <<< END
name = 'Tim O\'Reilly';
END;
$pattern = <<< END
' # opening quote
( # begin capturing
.*? # the string
(?<! \\\\ ) # skip escaped quotes
) # end capturing
' # closing quote
END;
preg_match( "($pattern)x", $input, $match);
echo $match[1];
Tim O\'Reilly
这里唯一的技巧是,要得到一个后向的模式来查看最后一个字符是否是反斜杠,我们需要转义反斜杠来防止正则表达式引擎看到表示右小括号的"\)"。也就是说,我们要在反斜杠前面再加上一个反斜杠:"\\)"。但是PHP中引用字符串的规则认为\\将生成一个单独反斜杠,所以我们需要通过正则表达式用4个反斜杠来得到一个反斜杠。这就是为什么大家都说正则表达式难以阅读的原因。
Perl限制后向只能用于固定长度的表达式上,即表达式不能包含量词,并且如果使用选择符(|),所有的选择都必须是相同长度。Perl兼容的正则表达式引擎也禁止在后向中使用量词,但是允许不同长度的选择。
4.10.11 剪切
Cut
我们很少使用的一次性子模式(once-only subpattern)或称剪切(cut),可以防止正则表达式在对待某些类型的模式时出现最坏的情况。一旦匹配,正则表达式就不会回溯子模式。
一次性子模式常用于自身重复的表达式:
/(a+|b+)*\.+/
下面的代码段用几秒钟的时间来报告匹配失败,效率很低:
$p = '/(a+|b+)*\.+$/';
$s = 'abababababbabbbabbaaaaaabbbbabbababababababbba..!';
if (preg_match($p, $s)) {
echo "Y";
} else {
echo "N";
}
这是因为正则表达式引擎试图在所有不同的地方开始匹配,但是不得不回溯每一个部分,这花费了很多时间。(译注2)如果你知道一旦有些地方被匹配它就不需要回头来解析(如本示例中出现了!已经说明不匹配了,再回溯已经没有意义),可以用(?>subpattern)来标记:
$p = '/(?>a+|b+)*\.+$/';
剪切不会改变匹配的结果,只是让它尽快报错。译注3
4.10.12 条件表达式
Conditional Expressions
在正则表达式中条件表达式就像一个if语句。一般格式为:
(?(condition)yespattern)
(?(condition)yespattern|nopattern)
如果断言成立,正则表达式引擎匹配yespattern。对于第二个形式,如果断言不成立,正则表达式引擎跳过yespattern并试图匹配nopattern。
断言可以是两种类型中的一种:逆向引用(backreference)或前向和后向匹配(lookahead和lookbehind match)。要引用一个之前匹配的子字符串,要求断言是从1到99中的数字(大多数的逆向引用都可以)。只有逆向引用被匹配时,条件才能使用断言中的模式。如果断言不是逆向引用,那么它必须是正或负的前向或后向断言。
4.10.13 正则表达式相关函数
Functions
有5类函数可用于Perl兼容正则表达式:匹配、替换、拆分、过滤和引用文本的通用函数。
4.10.13.1 匹配
preg_match( )函数执行Perl风格的模式来匹配字符串。它等效于Perl中的m//操作符。preg_match( )函数获得和ereg()函数一样的参数并给出相同的返回值,但接受的是Perl风格的模式而不是标准模式。
$found = preg_match(pattern, string [, captured ]);
例如:
preg_match('/y.*e$/', 'Sylvie'); //返回true
preg_match('/y(.*)e$/', 'Sylvie', $m); // $m 是 array('ylvie', 'lvi')
eregi( )函数执行不区分大小写的匹配,这里没有preg_matchi( )函数,而是在模式中使用i标志:
preg_match('y.*e$/i', 'SyLvIe'); //返回true
preg_match_all( )函数从最后一个匹配末尾重复地匹配,直到没有任何可匹配的为止:
$found = preg_match_all(pattern, string, matches [, order ]);
order参数值可以是PREG_PATTERN_ORDER 或 PREG_SET_ORDER,它用于决定数组matches中的布局。我们可以通过下面的代码来看这两种情况:
$string = <<< END
13 dogs
12 rabbits
8 cows
1 goat
END;
preg_match_all('/(\d+) (\S+)/', $string, $m1, PREG_PATTERN_ORDER);
preg_match_all('/(\d+) (\S+)/', $string, $m2, PREG_SET_ORDER);
使用PREG_PATTERN_ORDER(默认),则数组每个元素对应一个特定的捕获子模式。所以$m1[0]是包含所有匹配模式的子字符串的数组,$m1[1]是所有匹配第一个子模式(数字)的子字符串的数组,$m1[2]是所有匹配第二个子模式(单词)子字符串的数组。数组$m1中元素的个数比子模式个数多一个(译注4)。
使用PREG_SET_ORDER,则数组的每个元素对应尝试匹配整个模式的下一个,所以$m2[0]是匹配('13 dogs', '13', 'dogs')的第一个子集的数组,$m2[1] 是匹配('12 rabbits', '12', 'rabbits')的第二个子集的数组,依此类推。数组$m2的元素个数和成功匹配的模式数目相同。
示例4-2从一个特定web地址取出HTML放到一个字符串中,并从HTML中析取出URL。对于每个URL,将生成一个返回到程序的链接,该程序将在地址中显示URL。
示例4-2:从HTML页中析取URL
<?php
if (getenv('REQUEST_METHOD') == 'POST') {
$url = $_POST[url];
} else {
$url = $_GET[url]; }
?>
<form action="<?php echo $PHP_SELF ?>" method="POST">
URL: <input type="text" name="url" value="<?php echo $url ?>" /><br>
<input type="submit">
</form>
<?php
if ($url) {
$remote = fopen($url, 'r');
$html = fread($remote, 1048576); //读HTML的前1MB内容
fclose($remote);
$urls = '(http|telnet|gopher|file|wais|ftp)';
$ltrs = '\w';
$gunk = '/#~:.?+=&%@!\-';
$punc = '.:?\-';
$any = "$ltrs$gunk$punc";
preg_match_all("{
\b # 从单词边界处开始
$urls : # 需要资源和一个冒号
[$any] +? # 后跟一个或多个合法的
# 字符——但是是保守的,
#并且只包含你需要的。
(?= # 该匹配在
[$punc]* # punctuation
[^$any] # 后跟一个非URL字符
| # 或
$ # 符串的末尾处结束。
)
}x", $html, $matches);
printf("I found %d URLs<P>\n", sizeof($matches[0]));
foreach ($matches[0] as $u) {
$link = $PHP_SELF . '?url=' . urlencode($u);
echo "<A HREF='$link'>$u</A><BR>\n";
}
}
?>
4.10.13.2 替换
preg_replace( )函数的行为就像在文本编辑器中的查找和替换操作一样。它查找模式在字符串中所有出现的位置并把它们替换为其他的内容:
$new = preg_replace(pattern, replacement, subject [, limit ]);
最常见的用法是使用除整数参数limit之外所有的字符串参数。limit参数是模式出现最多的次数(默认情况下和限制为-1时,是所有位置)。
$better = preg_replace('/<.*?>/', '!', 'do <b>not</b> press the button');
// $better 为 'do !not! press the button'
传递一个字符串数组作为参数subject,可以替换所有元素中的字符串。新字符串由preg_replace( )返回:
$names = array('Fred Flintstone',
'Barney Rubble',
'Wilma Flintstone',
'Betty Rubble');
$tidy = preg_replace('/(\w)\w* (\w+)/', '\1 \2', $names);
// $tidy 为 array ('F Flintstone', 'B Rubble', 'W Flintstone', 'B Rubble')
要调用preg_replace( )在同一个字符串或字符串数组中执行多重替换,需要传递模式数组和用于替换的字符串:
$contractions = array("/don't/i", "/won't/i", "/can't/i");
$expansions = array('do not', 'will not', 'can not');
$string = "Please don't yellI can't jump while you won't speak";
$longer = preg_replace($contractions, $expansions, $string);
// $longer 是 'Please do not yellI can not jump while you will not speak';
如果给出的用于替换的字符串(上示例中的$expansions)比模式少,则匹配多出的模式的文本将被删除。这是一次删除大量数据的便捷方法:
$html_gunk = array('/<.*?>/', '/&.*?;/');
$html = 'é : <b>very</b> cute';
$stripped = preg_replace($html_gunk, array( ), $html);
// $stripped 是 ' : very cute'
如果给出一个模式数组,但是只有一个要替换的字符串,那么每个模式使用相同的替换:
$stripped = preg_replace($html_gunk, '', $html);
替换可以使用逆向引用,但是和模式中的逆向引用不同。在替换中逆向引用的首选语法是$1、$2、$3等,例如:
echo preg_replace('/(\w)\w+\s+(\w+)/', '$2, $1.', 'Fred Flintstone')
Flintstone, F.
/e修饰符使preg_replace( )把替换字符串当作PHP代码对待,返回在替换中使用的实际字符串。例如,下面把每个Celsius温度转换为Fahrenheit:
$string = 'It was 5C outside, 20C inside';
echo preg_replace('/(\d+)C\b/e', '$1*9/5+32', $string);
It was 41 outside, 68 inside
下面是更复杂的例子,它在字符串中扩展变量:
$name = 'Fred';
$age = 35;
$string = '$name is $age';
preg_replace('/\$(\w+)/e', '$$1', $string);
每一个匹配分隔变量名($name, $age)。替换中的$1引用这些名字,所以PHP代码实际上执行的是$name 和 $age。上面的代码可计算出用于替换的变量的值。
preg_replace( ) 的一个变种是 preg_replace_callback( )。它调用一个函数来对匹配模式的每个子字符串进行处理。函数的参数是一个由匹配模式的字符串组成的数组(第零个元素是匹配模式的所有字符串,第一个元素是第一个匹配子模式的内容,依此类推)。例如:
function titlecase ($s) {
return ucfirst(strtolower($s[0]));
}
$string = 'goodbye cruel world';
$new = preg_replace_callback('/\w+/', 'titlecase', $string);
echo $new;
Goodbye Cruel World
4.10.13.3 拆分
当你知道要提取的字符块是什么时,应使用preg_match_all( )来从字符串中析取字符块;当你知道用什么分隔字符块时,应使用preg_split( )来析取:
$chunks = preg_split(pattern, string [, limit [, flags ]]);
pattern 匹配两个字符块之间的分隔符。在默认情况下不返回分隔符。limit选项指定要返回字符块的最大数目(默认为-1,即所有字符块)。flags 参数 是对标志PREG_SPLIT_ NO_EMPTY (空字符串不返回) 和 PREG_SPLIT_DELIM_CAPTURE (在模式中捕获的部分字符串被返回)进行按位或操作(bitwise OR combination)的结果。
例如,要从一个简单的数字表达式中析取操作符可以这样做:
$ops = preg_split('{[+*/-]}', '3+5*9/2');
// $ops 是 array('3', '5', '9', '2')
要析取操作数和操作符,可以这样做:
$ops = preg_split('{([+*/-])}', '3+5*9/2', -1, PREG_SPLIT_DELIM_CAPTURE);
// $ops 是 array('3', '+', '5', '*', '9', '/', '2')
一个空模式匹配字符串中字符间的每个边界。这样你就可以把一个字符串拆分为一个字符数组:
$array = preg_split('//', $string);
4.10.13.4 使用正则表达式过滤数组
preg_grep( )函数返回与给定模式匹配的数组的所有元素:
$matching = preg_grep(pattern, array);
示例如,要得到以.txt结尾的文件名,可以使用:
$textfiles = preg_grep('/\.txt$/', $filenames);
4.10.13.5 引用正则表达式
preg_quote( )函数创建一个只匹配给定字符串的正则表达式:
$re = preg_quote(string [, delimiter ]);
在string中,每一个在正则表达式中有特定的含义的字符都以反斜杠开始:
echo preg_quote('$5.00 (five bucks)');
\$5\.00 \(five bucks\)
可选的第二个参数是被引用的额外字符。通常,可以用这个参数传递正则表达式的分隔符:
$to_find = '/usr/local/etc/rsync.conf';
$re = preg_quote($filename, '/');
if (preg_match("/$re", $filename)) {
//找到
}
4.10.14 和Perl正则表达式的差别
Differences from Perl Regular Expressions
虽然PHP中的Perl风格正则表达式和实际的Perl正则表达式非常相似,但它们之间还是有一些差别:
l 在一个模式字符串中null字符(ASCII值为0)不允许作为一个字符直接量。不过可以用其他方式引用它(\000、\x00等)。
l 不支持\E、\G、 \L、 \l、 \Q、 \u和 \U选项。
l 不支持(?{ some perl code })结构。
l 不支持/D、 /G、 /U、 /u、 /A和 /X修饰符。
l 垂直制表符\v被视为空白符。
l 前向和后向断言不能用*、 +或 ?来重复。
l 负断言内部加括号的子匹配不被记忆。
l 前向断言内部的选择分支(即选择符|左右的两部分)可以有不同的长度。译注5







