PHP 正则表达式

本文最后更新于:2024年3月18日 凌晨

PHP 正则表达式

  • 正则表达式是一个描述模式(pattern)的字符串,正则表达式函数会将模式和另一个字符串进行比较,检查字符串是否和模式匹配,一些函数会告诉你是否匹配,另一些则改变字符串。
  • PHP支持两种不同类型的正则表达式:POSIX正则表达式和Perl兼容(Perl=compatible)的正则表达式,POSIX正则表达式比Perl兼容的正则表达式功能弱,并且有时速度慢,但是易于阅读。
  • 正则表达式有3种作用:匹配从字符串提取信息,用新文本取代匹配的文本,把字符串拆分成小块的数组。
  • PHP为Perl和PISIX正则表达式中的这3种行为都提供了函数,例如,ereg()进行POSIX匹配,而preg_match()进行Perl匹配,幸运的是,基本的POSIX和Perl正则表达式之间有一些相似之处,所以我们再深入研究每个库的细节之前可以先介绍它们。

基础

  • 正则表达式中大部分字符都是直接量字符,这意味着它们只匹配自身,例如,如果在字符串"Dave was a cowhand"中查找正则表达式"cow",由于"cow"出现在字符串中,所以将得到匹配的结果。
  • 一些字符在正则表达式中有特殊的含义,例如,在正则表达式开头的^符号它必须匹配字符串的开头(或者更精确地说,是把正则表达式定位在字符串开头)
1
2
ereg('^cow','Dave was a cowhand');	// 返回flase
ereg('^cow','cowabunga!'); // 返回true
  • 类似地,一个在正则表达式末尾出现的美元符号$表示它必须匹配字符串的末尾(也就是说,把正则表达式定位在字符串的末尾)
1
2
ereg('cow$','Dave was a cowhand');	// 返回false
ereg('cow$',"Don't have a cow"); // 返回true
  • 在正则表达式中的句点.匹配任意字符。
1
2
3
4
5
ereg('c.t','cat');		// 返回true
ereg('c.t','cut'); // 返回true
ereg('c.t','c t'); // 返回true
ereg('c.t','bat'); // 返回false
ereg('c.t','ct'); // 返回false
  • 如果想要匹配特殊字符中的某一个(称为元字符,metacharacter),需要使用反斜杠对它进行转义:
1
2
ereg('\$5\.00','Your bill is $5.00 exactly');		// 返回true
erge('$5.00','Your bill is $5.00 exactly'); // 返回false
  • 正则表达式默认区分大小写,所以正则表达式"cow"和字符串"COW"是不匹配的,如果想要执行一个不区分大小写的POSIX风格的匹配,可以使用eregi()函数,要使用Perl风格的正则表达式,则可以使用preg_match(),但是需要指定一个标志来说明是不区分大小写的匹配。
  • 到目前为止,我们还没有做任何用字符串函数(比如strstr())无法处理的事,那为什么需要正则表达式呢?正则表达式的真正强大来自于它们可以指定抽象模式来匹配许多不同的字符序列,在正则表达式中可以指定3种基本的抽象模式:
    • 在字符串种可以出现的字符集(例如,字母,数字和特殊的标点符号)
    • 可选择的集合(例如,“com”,“edu”,“net"或"org”)
    • 在字符串中重复的序列(例如,至少一个但是不多于5个的数字字符)
    • 这3种模式可以按无数种方法结合在一起来创建正则表达式,以匹配像有效电话号码和URL之类的内容。

字符类

  • 要在模式中指定可接受的字符集,可以建立一个自己的字符类,可以通过用中括号包含可接受字符来建立自己的字符类:
1
2
3
4
ereg('c[aeiou]t','I cut my hand');		// 返回true
ereg('c[aeiou]t','This crusty cat'); // 返回true
ereg('c[aeiou]t','What cart?'); // 返回false
ereg('c[aeiou]t','14ct gold'); // 返回false
  • 正则表达式引擎发现一个c,然后检查下一个字符是否是a,e,i,o,u中的一个,如果它不是一个元音,则匹配失败并且引擎返回去查找另一个c,如果找到一个元音,引擎检查下一个字符是否为t,如果是的话,引擎达到匹配的末尾并返回true,如果下一个字符不是t,则引擎返回去查找另一个c
  • 可以在字符类开头使用^来否定该类:
1
2
3
ereg('c[^aeiou]t'],'I cut my hand');		// 返回false
ereg('c[^aeiou]t'],'Reboot chthon'); // 返回true
ereg('c[^aeiou]t'],'14ct gold'); // 返回false
  • 这里,正则表达式引擎查找一个c,然后是一个非元音字符,接着是一个t
  • 可以用连字符-定义一个字符范围,简单的字符类如所有字母和所有数字。
1
2
3
4
5
6
7
erge('[0-9]%','we are 25% complete');		// 返回true
ergr('[0123456789]%','we are 25% complete');// 返回true
ereg('[a-z]t','11th'); // 返回false
ereg('[a-z]t','cat'); // 返回true
ereg('[a-z]t','PIT'); // 返回false
ereg('[a-zA-Z]!','11!'); // 返回false
ereg('[a-zA-Z]!','stop!'); // 返回true
  • 当你指定一个字符类时,一些特殊字符就失去了它们的意义,同时其他字符有了新的意义,特别的是,当^字符不再是锚(anchor,就像之前讨论过的,用于指定以某个字符开头)而是用于否定字符类(当它是[的第一个字符串时)时,美元符$和句点.,也在字符类中失去了它们原来的意义($原指以某字符结尾,.原可代替任意字符),而仅仅只一个字符,例如,[^]]匹配任一不是右括号的字符,[$.^]匹配任意美元符号,句点,或^
  • 不同的正则表达式库为字符类定义了不同的缩写,包括数字,字母表的字符和空白符,POSIX风格和Perl风格的正则表达式在缩写语法上是有区别的,例如,POSIX用[[:space:]]表示空白类,而Perl则是\s

选择性

  • 可以用竖线|字符在正则表达式中指定可供选择的部分
1
2
3
ereg('cat|dog','the cat rubbed my legs');	// 返回true
ereg('cat|dog','the dog rubbed my legs'); // 返回true
ereg('cat|dog','the rabbit rubbed my legs');// 返回false
  • 选择的优先级会让人惊讶:^cat|dog$选自^catdog$,意味着它匹配以cat开头或dog结尾的一行,如果想要一行只包括"cat"或"dog",需要使用正则表达式^(cat|dog)$
  • 可以组合字符类和选择符号,例如,为字符串作不以大写字母开头的检查:
1
2
3
ereg('^([a-z]|[0-9])','The quick brown fox');	// 返回false
ereg('^([a-z]|[0-9])','jumped over'); // 返回true
ereg('^([a-z]|[0-9])','10 lazy dogs'); // 返回true

重复序列

  • 要指定一个重复模式,要使用量词(quantifier),量词在重复模式后面,用来说明要重复这个模式多少次,下表显示了POSIX和Perl正则表达式都支持的量词。
量词
? 0或1
* 0或更多
+ 1或更多
{n} n次
{n,m} 至少n次,不超过m次
{n,} 至少n次
  • 要重复单个字符,只要简单地把量词放在字符后面:
1
2
3
4
ereg('ca+t','caaaaaaaaaat');		// 返回true
ereg('ca+t','ct'); // 返回false
ereg('ca?t','caaaaaaaaaat'); // 返回false
ereg('ca*t','ct'); // 返回true
  • 使用量词和字符类,我们可以实际地做一些有用的事情,例如匹配有效的美国电话号码:
1
2
ereg('[0-9]{3}-[0-9]{3}-[0-9]{4}','303-555-1212);// 返回true
ereg('[0-9]{3}-[0-9]{3}-[0-9]{4}','64-9-555-1234);// 返回false

子模式

  • 可以使用小括号把几个正则表达式组合在一起作为一个单独的单元来对待,这个单元被称为子模式(subpattern)
1
2
3
ereg('a (very )+big dog','it was very very big dog');	// 返回true
ereg('^(cat|dog)$','cat'); // 返回true
ereg('^(cat|dog)$','dog'); // 返回true
  • 小括号也使得与子模式的匹配的子串被捕获,如果把一个数组当作第三个参数传递给匹配函数,任何捕捉到的字串都将被放进该数组:
1
ereg('([0-9]+)','You have 42 magic beans',$captured); // 返回true并对$captured进行赋值。
  • 数组的第零个元素设置为匹配的整个字符串,第一个元素是与第一个子模式匹配的子串(如果有一个的话),第二个元素是与第二个子模式匹配的子串,依此类推。

POSIX风格的正则表达式

  • 你已经了解了正则表达式的基础知识,接下来我们可以讨论一下其细节,POSIX风格的正则表达式使用Unix地区系统(locale system),该系统提供了排序和识别字符的函数来让你智能地处理其他非英语文本,特别地,各种语言组成单词的"字母"(如à和è)不同,POSIX正则表达式考虑到了这一点并提供相应的字符类。
  • 然而,POSIX正则表达式是为使用仅有原文的数据而设计的,如果数据中有空字节(\x00),那么正则表达式函数把它理解为字符串的末尾,并且匹配不会超过该字节位置,要匹配任意的二进制数据,需要使用本章稍后讨论的兼容Prel的正则表达式,正如我们已经提到的,Perl风格的正则表达式函数常常比等效的POSIX风格的函数快。

字符类

  • 如下表所示,POSIX定义了许多可以在字符类中使用的命名字符集,在下表中给出的拓展是英语的,实际的字母根据使用者地区(locale)不同而不同。
描述 扩展
[:alnum:] 字母和数字字符 [0-9a-zA-Z]
[:alpha:] (letters)字母字符(字母) [a-zA-Z]
[:ascii:] 7位ASCII [\x01-\x7F]
[:blank:] 水平空白符(空格,制表符) [ \t]
[:cntrl:] 控制字符 [\x01-\x1F]
[:digit:] 数字 [0-9]
[:graph:] 用墨水打印的字符(非空格,非控制字符) [^\x01-\x20]
[:lower:] 小写字母 [a-z]
[:print:] 可打印字符(圆形类加空格和制表符) [\t\x20-\xFF]
[:punct:] 任意标点符号,如句点(.)和分号(😉 [-!”#$%&’()*+,./:;<=>?@[\\\]^_’{|}~]
[:space:] 空白(换行,回车,制表位,空格,垂直制表位) [\n\r\t \x0B]
[:upper:] 大写字母 [A-Z]
[:xdigit:] 十六进制数字 [0-9a-fA-F]
  • 每一个[:something:]类都可被用于替补一个字符类中的字符,例如,要查找任一数字字符,大写字母或一个@符号,可以使用下面的正则表达式:
1
[@[:digit:][:upper:]]
  • 但是,不能把一个字符类当作一个范围的终点使用:
1
ereg('[A-[:lower:]]','string');		// 非法的正则表达式。
  • 一些地区把某些字符序列当作一个单独的字符来考虑,它们被称为排序序列(collating swquence),在字符类中匹配这些多字符序列中的一个时,要把它用[..]括起来。
  • 例如,如果你的地区有排序序列ch,你可以使用下面的字符类类匹配s,t,ch:
1
[st[.ch.]]
  • POSIX最后的字符类扩展是等价类(equivalence class),把字符用[==]括起来指定,等价字符类匹配有相同整理顺序的字符(由当前locate定义),例如,一个地区可能定义a,āá有相同的排列优先级,要匹配它们中的一个,等价类为[=a=]

  • 锚(anchor)将匹配限制在字符串中特定位置(锚匹配目标字符串中的实际字符),列出了POSIX正则表达式支持的锚。
匹配
^ 字符串开始
$ 字符串末尾
[[:<:]] 单词开始
[[:>:]] 单词末尾

单词边界指在一个空白符和一个字符串和一个标识符(字母数组或下划线)之间的那一点:

1
2
ereg('[:<;]gun[[:>:]]','the Burgundy exploded');	// 返回false
ereg('gun','the Burgundy exploded'); // 返回true
  • 注意:字符串的开头和结尾也可以作为单词边界。

函数

  • POSIX风格的正则表达式有3类函数:匹配,替换和拆分。

匹配

  • ereg()函数接收一个模式,一个字符串和一个可选的数组,如果给定数组的话就组装数组,并根据是否在字符串中找到模式的一个匹配而返回true或false
1
$found = ereg(pattern,string[,captured]);
  • 例如:
1
2
ereg('y.*e$','Sylvie');		// 返回ture
ereg('y(.*)e$','Sylvie',$a); // 返回true,$a是array('Sylvie','lvi')
  • 数组的第零个元素设置为与之匹配的整个字符串,第一个元素是与第一个子模式匹配的子字符串,第二个元素是与第二个子模式匹配的子字符串,依此类推。
  • eregi()函数是不区分大小写的ereg(),它的参数和返回值都和ereg()相同。
  • 下例使用模式匹配来确定一个信用卡号是否通过了Luhn校验,以及该数字是否符合特定的信用卡格式。

示例4-1:信用卡验证。

luhn验证可以判断一个信用卡号是否遵循正确的校验,但是它不能告知该卡号是否已经发行,当前是否为活动的或有足够的空间来存入金额。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function IsVaildCreditCard($inCardNumber,$inCardType){
// 假设是正确的。
$isValid = true;

// 从字符串中删除所有非数字字符。
$inCardNumber = ereg_replace('[^:digit:]]'.'',$inCardNumber);

// 确认卡号和类型匹配。
switch($inCardType){
case 'mastercard':
$isValid = ereg('^5[1-5].{14}$',$inCardNumber);
break;
case 'visa':
$isValid = ereg('^4.[15]$|^4.{12}$',$inCardNumber);
break;
case 'amex':
$isValid = ereg('^3[47].{13}$',$inCardNumber);
break;
case 'discover':
$isValid = ereg('^6011.{12}$',$inCardNumber);
break;
case 'diners':
$isValid = ereg('^30[0-5].{11}$|^3[68].{12}$',$inCardNumber);
break;
case 'jcb':
$isValid = ereg('^3.{15}$|^2131|1800.{11}$',$inCardNumber);
break;
}

// 通过了基本的测试,现在进行Luhn校验。
if($isValid){
// 翻转。
$inCardNumber = strrev($inCardNumber);
// 合计数字的总位数,在奇数位加倍。
$theTotal = 0;
for($i = 0;$i < strlen($inCardNumberr);$i++){
$theAdder = (int)$inCardNumber[$i];
// 使在奇数位的数字加倍。
if($i % 2){
$theAdder = $theAdder << 1;
if($theAdder > 9){
$theAdder -= 9;
}
$theTotal += $theAdder;
}
}
// 合法的卡号除以10
$isValid = (($theTotal % 10) == 0);
}
return $isValid;
}

替换

  • ereg_replace()函数的参数为一个模式,一个替换字符串和一个要在其中进行查找的字符串,函数返回查找字符串的一个拷贝,字符串中与模式匹配的文本用替换字符串来代替。
1
$changed = ereg_replace(pattern,replacement,string);
  • 如果该模式有任何分组的子模式,则该匹配可以通过把字符\1到\9放到替换字符串中来访问,例如,可以使用ereg_replace()以等价的HTML标签替换用[b]和[/b]标签包含的字符:
1
2
3
$string = 'It is [b]not[/b] a matter of diplomacy.';
echo ereg_replace ('\[b]([^[]*])\[/b]','<b>\1</b>',$string);
It is<b>not</b> a matter of diplomacy
  • eregi_replace()函数是ereg_replace()不区分大小写的形式,它的参数和返回值和ereg_replace()相同。

拆分

  • split()函数使用正则表达式来把字符串拆分成较小的块,作为一个数组返回,如果出现错误,则split()返回false,可以选择要返回多少个小块:
1
$chunks = split(pattern,string[,limit]);
  • 本函数的模式用于匹配分隔成块的文本,例如,从一个算术表达式中拆分项:
1
2
3
$expression = '3*5+i/6-12';
$terms = split('[/+*-]',$expression);
//$terms是array('3','5','i,,6,'12')
  • 如果指定了一个匹配数目限制,则数组的最后一个元素会存放剩余的字符串:
1
2
3
$expression = '3*5+i/6-12';
$terms = split('[/+*-]',$expression,3);
//$terms是array('3','5','i/6-12')
  • 一直以来,Perl被视为强大的正则表达式的标准,PHP使用一个被称为pcre的C库,几乎完全支持perl正则表达式的特性,Perl正则表达式包括之前介绍过的POSIX类和锚。
  • 在Perl正则表达式中,POSIX风格的字符类可以操作和理解使用Unix地区系统的非英语字符,Perl正则表达式可以作用于任意的二进制数据,所以你可以安全地对带有空字节(NUL-byte,\x00)的模式或字符串进行匹配。

兼容Perl风格的正则表达式

分隔符

  • Perl风格的正则表达式模仿的语法,即每个模式都必须用一对分隔符括起来,习惯上使用左斜杠/,例如/pattern/,不过,任意非数字字母的字符(除了反斜杠\)都可以用于分割一个Perl风格的模式,这在匹配包含斜杠的字符串是很有用的,如文件名,例如,下面的语句是等效的:
1
2
3
preg_match('/\/usr\/local\//','/usr/local/bin/perl');	// 返回true

preg_match('#/usr/local/#','/usr/local/bin/perl'); // 返回true
  • 小括号(),大括号{},中括号[]和尖括号<>可被作为模式分隔符使用:
1
preg_match('{/usr/local/}','/usr/local/bin/perl');	// 返回true
  • 后缀选项:放在结束分隔符后面的单个字符修饰符,它用于修改正则表达式引擎的行为。
  • 非常有用的一个是x,它可以让正则表达式引擎在匹配前从正则表达式中跳过空白符和被#标记的注释,下面这两种模式是相同的,但是其中一个会更易于阅读:
1
2
3
4
5
6
7
8
'/([[:alpha:]]+)\s+\1/'

'/( # start capture
[[:alpha:]]+ # a word
\s+ # whitespace
\1 # the same word again
) # end capture
/x'.

匹配行为

  • 虽然Perl正则表达式语法包括我们之前谈到的POSIX结构,但在Perl中一些模式组件有不同的意义,Perl正则表达式特别为单行文字匹配进行了优化(虽然有一些选项来改变这个行为)
  • 句点.匹配任意除换行符\n之外的字符,美元符号$匹配字符串的末尾或在换行符之前以换行符结尾的字符串。
1
preg_match('/is(.*)$/',"the key is in my pants",$captured);	//$captured[1]是''in my pants'

字符类

  • Perl风格的正则表达式不仅支持POSIX字符类,而且定义了一些自己的字符类,如下表所示:
字符类 意义 拓展
\s 空白符 [\r\n \t]
\S 非空白符 [^\r\n \t]
\w 单词(标识符)字符 [0-9A-Za-z_]
\W 非单词(标识符)字符 [^0-9A-Za-z_]
\d 数字 [0-9]
\D 非数字 [^0-9]

  • Perl风格的正则表达式也支持附加锚(其自定义的锚标记),如下表所示:
断言 意义
\b 单词边界(在\w和\W之间或在字符串开头或末尾)
\B 非单词边界(在\w和\w或\W和\W之间)
\A 字符串开头
\Z 字符串末尾或在末尾的\n之前
\z 字符串末尾
^ 行的开头(或如果/m标志启用的话在\n之后)
$ 行的末尾(或如果/m标志启用的话在\n之前)

量词和贪婪性

  • Perl也支持POSIX量词,而且是具有贪婪性的(greedy),即当有一个量词时,引擎在仍然满足匹配模式的情况下尽可能多地进行匹配,例如:
1
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提供了一组用于最小匹配的量词,它们很容易记住,因为它们和贪婪量词相同,只是附加了一个问号?,下表显示了Perl风格正则表达式中相应的贪婪量词和非贪婪量词。
贪婪量词 非贪婪量词
? ??
* *?
+ +?
{m} {m}?
{m,} {m,}?
{m,n} {m,n}?
  • 下面是如何使用一个非贪婪量词来匹配标签:
1
preg_match('/(<.*?>)/','do <b>not</b> press the button',$match);	//$match[1]为'<b>'
  • 另外一个更快的方法是使用一个字符类来匹配每个非大于字符到下一个大于字符:
1
preg_match('/(<[^>]*>)/','do <b>not</b> press the button',$match);	//$match[1]为'<b>'

非捕获匹配

  • 如果把模式的一部分用小括号括起来,那么匹配子模式的文本被捕获并且可以在后面访问,但是有时你想创建一个不捕获匹配文字的子模式,那么在Perl兼容正则表达式中可以使用?:subpattern结构来这样做:
1
preg_match('/(?:ello)(.*)/','jello biafra',$match);	//$match[1]是'biafra'

逆向引用

  • 可以使用一个逆向引用(backreference)来引用模式中之前被捕获的字符串:\1引用第一个子模式的内容,\2引用第二个,以此类推,如果嵌套了子模式,那么第一个引用以第一个左小括号开始,第二个引用以第二个左小括号开始,以此类推。
  • 例如,下面识别两倍的单词:
1
2
preg_match('/([[:alpha:]]+)\s+\1/','Paris in the thespring',$m);
// 返回true并且$m[1]是'the'
  • 不能捕获超过99个子模式。
  • 逆向引用最有用的功能可以确定文字中连续出现两个相同单词的位置。
  • 如本例中:
    • 第一次匹配模式Paris\1,Paris前面没有被捕获的字符串,故放弃。
    • 第二次匹配in\1,\1引用前面被捕获的字符串Paris,故合并后为in Paris,没有找到,放弃。
    • 第三次匹配the\1,前面捕获的字符串in,故合并后为the in,未找到,放弃。
    • 第四次匹配the\1,前捕获的字符串为the,故合并后为the the,找到字符串停止匹配,并将the the存入数组$m的0号元素,将the存入数组$m的1号元素故$m[1]the

后缀模式

  • Perl风格的正则表达式允许把单个字符选项(标志)放在正则表达式模式后面来修改匹配的解释或行为,例如,要进行不区分大小写的匹配,可以简单地使用i标志:
1
preg_match('/cat/i','Stop, Catherine!');	// 返回true
  • 下表显示了在Perl兼容正则表达式中支持的来自Perl的修饰符。
修饰符 意义
/regexp/i 不区分大小写的匹配
/regexp/s 使句点(.)匹配任何字符,包括换行符(\n)
/regexp/x 从模式中删除空白符和注释
/regexp/m 使^匹配换行符(\n)之后的内容,美元符号($)匹配换行符(\n)之前的内容
/regexp/e 如果替换字符串是PHP代码,使用eval()执行该代码来得到实际的替换字符串
  • PHP的Perl兼容正则表达式函数也支持在Perl中不支持的其他修饰符,如下表所示:
修饰符 意义
/regexp/U 颠倒子模式的贪婪性:*和+尽可能少地匹配而不是尽可能多
/regexp/u 把模式字符串当作UTF-8编码对待
/regexp/X 如果一个反斜杠之后跟着没有特殊意义的字符,将产生一个错误
/regexp/A 把锚定位在字符串的开头就像模式中有^一样
/regexp/D 使$字符仅匹配一行的末尾
/regexp/S 使表达式解析器更加小心地检查模式的结构,使得第二次运行时(如在一个循环中)加快速度
  • 在一个模式中可以使用多个选项,如下所示:
1
2
3
4
5
6
7
8
9
$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'

内联选项

  • 除了在模式结束分隔符之后指定模式选项之外,还可以在一个模式内部指定仅运用于部分模式的选项,语法如下:
1
(?flags:subpattern)
  • 例如,在这个示例中只有单词"PHP"是不区分大小写的:
1
preg_match('/I like (?i:PHP)/','I like php');	// 返回true
  • i,m,s,U,x和X选项可被用在这种方式的内部,一次可以使用多个选项:
1
preg_match('/eat (?ix:fo    o   d)/'.'eat FoOD');// 返回true
  • 一个选项前如果有连字符-表示关闭此选项:
1
preg_match('/(?-i:I like) PHP/i','I like pHp');		// 返回true
  • 可以启用或禁用标志直到封闭的子模式或模式末尾:
1
2
preg_match('/I like (?i)PHP/','I like pHp');	// 返回true
preg_match('/I (like (?i)PHP) a lot/','/I like pHp a lot',$match); //$match[1]是'like pHp'
  • 内置标志不能用于捕获字符串,需要设置一个附加的小括号来完成捕获(如上示例中用小括号定义了一个子模式)

前向和后向断言

  • 有时在模式中能够指出"如果这里是下一个,就匹配这里"是很有用的,这在拆分字符串时是很常见的,这种正则表达式描述了分隔符,但不返回,可以使用前向断言(lookahead assertion)来确保在分隔符之后有更多的数据(因为没有匹配它,所以阻止被返回),类似的,后向断言(lookbehind assertion)检查前面的文字。
  • 前向和后向各有两种形式:正(positive)和负(negative),正的前向或后向表示"下一个/前面的文本必须如此",负的前向和后向表示"下一个/前面的文本必须不是这样",下表展示了在Perl兼容模式中可以使用的4中结构,这4中结构都不捕获文本。
结构 意义
(?=subpattern) 正前向
(?!subpattern) 负前向
(?<=subpattern) 正后向
(?<!subpattern) 负后向
  • 正前向的一个简单应用是将一个Unix mbox邮件文件分解成单独的消息,单词"From"通过自己开始一行来指出一条新消息的开始,所以可以通过在一行开始处指定分隔符作为下一个文本中"From"的位置来把mailbox拆分为消息:
1
$message = preg_split('/(?=^From)/m',$mailbox);
  • 负后向的一个简单应用是析取包含引用分隔符的引用字符串,例如,下面的例子是告诉你如何析取一个用单引号括起来的字符串(注意正则表达式使用x修饰符,可以对模式加上注释);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$input = <<< END
name = 'Tim O\'Rilly';
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兼容的正则表达式引擎也禁止在后向中使用量词,但是允许不同长度的选择

剪切

  • 我们很少使用的一次性子模式(once-only subpattern)或称剪切(cut),可以防止正则表达式在对待某些类型的模式时出现最坏的情况,一旦匹配,正则表达式就不会回溯子模式。
  • 一次性子模式常用于自身重复的表达式:
1
/(a+|b+)*\.+/
  • 下面的代码段用几秒钟的时间来报告匹配失败,效率很低:
1
2
3
4
5
6
7
$p = '/(a+|b+)*\.+$/';
$s = 'abababababbabbbabbaaaaaabbbbabbabababababbba..!';
if(preg_match($p,$s)){
echo "Y";
}else{
echo "N";
}
  • 这是因为正则表达式引擎试图在所有不同的地方开始匹配,但是不得不回溯每一个部分,这花费了很多时间,如本例中,匹配到感叹号!时正则表达式已经发现了不匹配子模式,但是不会停止,而是回过头来将之前的量词减一再重新进行匹配,这样的回溯过程导致了以上代码效率非常低下。
  • 如果你知道一旦有些地方被匹配它就不需要回头来解析(如本例子出现了!已经说明不匹配了,再回溯已经没有意义),可以用(?>subpattern)来标记:
1
$p = '/(?>a+|b+)*\.+$/';
  • 剪切不会改变匹配的结果,只是让它尽快报错,使用一次性子模式,一旦找到感叹号!就立即停止匹配而直接报错,效率提高。

条件表达式

  • 在正则表达式中的条件表达式就像一个if语句,一般格式为:
1
2
(?(condition)yespattren)
(?(condition)yespattern|nopattern)
  • 如果断言成立,正则表达式引擎匹配yespattern,对于第二个形式,如果断言不成立。正则表达式引擎跳过yespattern并试图匹配nopattern
  • 断言可以是两种类型中的一种:逆向引用(backreference)或前向和后向匹配(liikahead and lookbehind match),要引用一个之前匹配的子字符串,要求断言是从1到99中的数字(大多数的逆向引用都可以),只有逆向引用被匹配时,条件才能使用断言中的模式,如果断言不是逆向引用,那么它必须时正或负的前向或后向断言。

正则表达式相关函数

有5类函数可用于Perl兼容正则表达式:匹配,替换,拆分,过滤和引用文本的通用函数。

匹配

  • preg_match()函数执行Perl风格的模式来匹配字符串,它等效于Perl中的m//操作符。
  • preg_match()函数获得和ereg()函数一样的参数并给出相同的返回值,但接受的是Perl风格的模式而不是标准模式。
1
$found = preg_match(pattern,string[,captured]);
  • 例如:
1
2
preg_match('/y.*e$/','Sylvie');		// 返回true
preg_match('/y(.*)e$/','Sylvie',$m);//$m时array('ylvie','lvi')
  • eregi()函数执行不区分大小写的匹配,这里没有preg_matchi()函数,而是在模式中使用i标志:
1
preg_match('Y.*c$/i','SyLvIe');		// 返回true
  • preg_match_all()函数从最后一个匹配末尾重复地匹配,直到没有任何可匹配的为止:
1
$found = preg_match_all(pattern,string,matches[,order]);
  • order参数值可以是PREG_PATTERN_ORDERPREG_SET_ORDER,它用于决定数组matches中的布局,我们可以通过下面的代码来看这两种情况:
1
2
3
4
5
6
7
8
$string = <<< END
13 dogs
12 rabbits
8 cows
1 goat
END;
preg_match_all('/(\d+)(\S+)/',$string,$m1,PREG_PATTREN_ORDER);
preg_match_all('/(\d+)(\S+)/',$string,$m2,PREG_SET_ORDER);
  • 使PREG_PATTERN_ORDER(默认)则数组每个元素对应一个特定的捕获子模式,所以$m1[0]是包含所有匹配模式的子字符串的数组,$m1[1]是所有匹配第一个子模式(数字)的子字符串的数组, $m1[2]是所有匹配第二个子模式(单词)子字符串的数组,数组$m1中元素的个数比子模式个数多一个,多了一个0号元素,其他的一一对应。
  • 使用PREG_SET_ORDER,则数组的每个元素对应尝试匹配整个模式的下一个,所以$m2[0]是匹配'13dogs','13','dogs'的第一个子集的数组,$m2[1]是匹配'12 rabbits','12','rabbits'的第二个子集的数组,依此类推,数组$m2的元素个数和成功匹配的模式数目相同。
  • 下例从一个特定Web地址取出HTML放到一个字符串中,并从HTML中析取出URL,对于每个URL,将生成一个返回到程序的链接,该程序将在地址中显示URL

示例4-2:从HTML页中析取URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
if (getenv('REQUEST_METHOD') == 'POST'){
$url = $_POST[url];
}else{
$url = $_GET[url];}
?>

<form action="<?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";
}
}
?>

替换

  • preg_replace()函数的行为就像在文本编辑器中的查找和替换操作一样,它查找模式在字符串中所有出现的位置并把它们替换为其他的内容:
1
$new = pref_replace(pattern,replacement,subject[,limit]);
  • 最常见的用法是使用除整数参数limit之外所有的字符串参数,limit参数是模式出现最多的次数(默认情况下和限制为-1时,时所有位置)
1
better = preg_replace('/<.*?>/','!','do<b>not</b>press the button');	//$better为'do !not! press the button'
  • 传递一个字符串数组作为参数subject,可以替换所有元素中的字符串,新字符串由preg_replace()返回:
1
2
3
4
5
$name = 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()在用一个字符串或字符串数组中执行多重替换,需要传递模式数组和用于替换的字符串:
1
2
3
4
$contractions = array("/dont't/i","/won't/i","/can't/i");
$expansions = array('do not','will not','can not');
$string = "Please don't yell--I can't jump while you won't speak";
$longer = preg_replace($contractions,$expansions,$string); //$longer是'Please do not yell--I can not jump while you will not speak';
  • 如给出的用于替换的字符串(上示例中的$expansions)比模式少,则匹配多出的模式的文本将被删除,这是一次删除大量数据的便捷方法:
1
2
3
4
$html_gunk = array('/<.*?>/','&.*?;/');
$html = '&eacute; : <b>very</b> cute';
$stripped = preg_replace($html_gunk,array(),$html);
//$stripped是' : very cute'
  • 如果给出一个模式数组,但是只有一个要替换的字符串,那么每个模式使用相同的替换:
1
$stripped = preg_replace($html_gunk, '',$html);
  • 替换可以使用逆向引用,但是和模式中的逆向引用不同,在替换中逆向引用的首选语法是$1,$2,$3等,例如:
1
2
3
echo preg_replace('/(\w)\w+\s+(\w+)/','$2,$1.','Fred Flintstone');

FLintstone, F.
  • /e修饰符使preg_replace()把替换字符串当作PHP代码对待,返回在替换中使用的实际字符串,例如,下面把每个Celsius温度转换为Fahrenheit:
1
2
3
4
$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
  • 下面是更复杂的例子,它在字符串中扩展变量。
1
2
3
4
$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(),它调用一个函数来对匹配模式的每个子字符进行处理,函数的参数是一个由匹配模式的字符串组成的数字(第零个元素是匹配模式的所有字符串,第一个元素是第一个匹配子模式的内容,依此类推),例如:
1
2
3
4
5
6
7
8
function titlecase($s){
return ucfirst(strtolower($s[0]));
}
$string = 'goodbye cruel world';
$new = preg_replace_callback('/\w+/','titlecase',$string);
echo $new;

Goodbye Cruel World

拆分

  • 当你知道要提取的字符块是什么时,应使用preg_match_all()来从字符串中析取字符块,当你知道用什么分隔字符块时,应使用pref_split()来析取:
1
$chunks = preg_split(pattern,string[,limit[,flags]]);
  • pattern匹配两个字符块之间的分隔符,在默认情况下不返回分隔符,limit选项指定要返回字符块的最大数目(默认为-1,即所有字符块),flags参数是对标志PREG_SPLIT_NO_EMPTY(空字符串不返回)PREG_SPLIT_DELIM_CAPTURE(在模式中捕获的部分字符串被返回)进行按位或操作(bitwise OR combination)的结果。
  • 例如,要从一个简单的数字表达式中析取操作符可以这样做:
1
$ops = preg_splist('{[+*/-]}','3+5*9/2');	//$ops是array('3','+','5','*','9','/','2')
  • 一个空模式匹配字符串中字符间的每个边界,这样你就可以把一个字符串拆分为一个字符数组:
1
$array = preg_split('//',$string);

使用正则表达式过滤数组

  • preg_grep()函数返回与给定模式匹配的数组的所有元素:
1
$matching = preg_grep(pattern,array);
  • 例如,要得到以.txt结尾的文件名,可以使用:
1
$textfiles = preg_grep('/\.txt$/',$filenames);

引用正则表达式

  • preg_quote()函数创建一个只匹配给定字符串的正则表达式:
1
$re = preg_quote(string [,delimiter]);
  • 在string中,每一个在正则表达式中有特定的含义的字符都以反斜杠开始:
1
2
echo preg_quote('%5.00 (five bucks)');
\$5\.00 \(five bucks\)
  • 可选的第二个参数是被引用的额外字符,通常,可以用这个参数传递正则表达式的分隔符:
1
2
3
4
5
$to_find   ='/usr/local/ect/rsync.conf';
$re = preg_quote($filename,'/');
if(preg_match("/$re",$filename)){
// 找到。
}

和Perl正则表达式的差别

  • 虽然PHP中的Perl风格正则表达式和实际的Perl正则表达式非常相似,但它们之间还是有一些差别:
    • 在一个模式字符串中null字符(ASCII值为0)不允许作为一个字符直接量,不过可以用其他方式引用它(\000,\x00等)
    • 不支持\E, \G, \L, \l, \Q, \u和 \U选项。
    • 不支持(?{some perl code})结构。
    • 不支持/D, /G, /U, /u, /A和 /x修饰符。
    • 垂直制表符\v被视为空白符。
    • 前向和后向断言不能用*,+或?来重复。
    • 负断言内部加括号的子匹配不被记忆。
    • 前向断言内部的选择分支(即选择符|左右的两部分)可以有不同的长度

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!