正则表达式匹配规则

前面已经讲过"*","-","?" 等元字符,它们都有各自的特殊含义。如果想匹配没有预定义元字符的字符集合,或者表达式和已知定义相反,或者存在多种匹配情况,应该怎么办?

字符组

查找数字、字母、空白很简单,因为已经有了对应这些字符集合的元字符,但是如果想匹配没有预定义元字符的字符集合(比如元音字母a、e、i、o、u),方法很简单,只需要在方括号里列出它们。

例如[aeiou] 匹配任何一个英文元音字母,[.?!] 匹配标点符号(”.”、”?”、”!”),c[aou]t 匹配”cat”,”cot”,”cut”这三个单词,而”caout”则不匹配。

注意:[]匹配单个字符,尽管看起来[]里有好多字符。

也可以指定字符范围,例如[0-9]的意义与\d 完全一致:代表以为数字;同理[a-z0-9A-Z_] 完全等价于 \w(如果只考虑英文)。

字符组很简单,但是一定要弄清楚字符组中什么时候需要转义。

转义

如果想查找或匹配元字符本身,比如查找*、?等就出现问题:没办法指定,因为它们会被解释成别的意思。这时就使用\来取消这些字符的特殊意义。因此,应该使用\.\*。当然,查找\本身用\\。这叫做转义。

通俗地讲,转义就是防止特殊字符被解析,或者说用某个符号表示另一个特殊符号。例如:unibetter\.com匹配unibetter.com,C:\\Windows匹配C:\Windows

在JavaScript或者PHP中都接触过转义的概念。例如,JavaScript中要弹出一个对话框,对话框中需要分成两行显示,用HTML的<br>标签或者在源代码里手工换行都不行,应该用\r\n表示换行并新起一行,如下所示:

alert("警告<br>操作无效");//错误
alert("警告\r\n操作无效");//正确

在PHP里使用反斜杠(\)表示转义,\Q和\E也可以在模式中忽略正则表达式元字符,比如:

\d+\Q.$.\E$

以上表达式先匹配一个或多个数字,紧接着一个点号,然后一个,再然后一个点号,最后是字符串末尾。也就是说,\Q和\E之间的元字符都会作为普通字符用来匹配。

正则表达式是不是遇到这些特殊字符就该转义呢?答案显然是否定的。转义只有在一定条件下,比如可能引起歧义或者被误解析的情况下才需要。有些情况并不需要转义这些“特殊”字符,并且在时转义也是无效的。这需要不断尝试并积累经验。看一个例子:

<?php
$reg = "#[aby\}]#";
$str = 'a\bc[]{}';
preg_match_all($reg,$str,$m);
var_dump($m);

运行结果为

array(1) { [0]=> array(3) { [0]=> string(1) "a" [1]=> string(1) "b" [2]=> string(1) "}" } } 

在字符组中匹配”a”,”b”,”y”和”}”中任意一个,由于"}"是元字符,具有特殊意义,所以这里进行了转义,使用"\}"表示"}"

但是实际上,这个转义是多余的。虽然”}”是元字符,具有特殊意义,但是
在字符组中,”}”却无法发挥意义,不会引起歧义,所以不需要转义。在这里"\}""}"是等价的。

既然转义符"\"是多余的,那么会不会被当作普通字符呢?字符串str里有"\",但是可以从代码运行结果中看出,"\"字符并没有被匹配,也就是说正则表达式“#[aby\}]#”中,虽然"\"转义符是多余的,但是也并没有被当作普通字符进行匹配。

如果确实要把"\"当作普通字符匹配,正则表达式需要写成:

<?php
$reg = "#[aby\\\}]#";
$str = 'a\bc[]{}';
preg_match_all($reg,$str,$m);
var_dump($m);

运行结果为:

array(1) { [0]=> array(4) { [0]=> string(1) "a" [1]=> string(1) "\" [2]=> string(1) "b" [3]=> string(1) "}" } } 

前面提到,不是所有出现特殊字符的地方都要转义。例如,以下正则表达式可以匹配“cat”、“c?t”、“c)t”等字符:

c[aou?*)]t

其中"?""*"等特殊字符都不需要转义。原因很简单,字符组里匹配的是单个字符,这些特殊字符不会引起歧义。

字符组里可以使用转义吗?可以,例如"c[\d]d"可以匹配“c1d”、“c2d”等。下面是复杂的表达式:

\(?0\d{2}[) -]?\d{8}

“(“和”)”也是元字符,所以在这里需要转义。这个表达式可以匹配几种格式的电话号码,例如(010)11223344、012-11223344或01211223344等。首先是转义符”(“,表示出现0次或1次(?),然后是一个0,后面跟着两个数字(\d{2}),然后是”)”、”-“或空格中的一个,出现出现0次或1次(?),最后是8个数字(\d{8})。

反义

有些时候,查找的字符不属于某个字符类,或者表达式和已知定义相反(比如除了数字以外其他任意字符),这时需要用到反义。常用反义如表所示。

示例

反义有个比较明显的特征,就是和一些已知元字符相反,并且为大写形式。比如”\d”表示数字,而”\D”就是表示非数字。例如

1)匹配不包含空白符的字符串

\S+

2)用尖括号括起来,以a开头的字符串

<a[^>]+>

比如,要匹配字符串"<a href='http://baidu.com'>百度</a>" ,这个正则表达式的匹配结果就是"<a href='http://baidu.com'>"

提示:”^”在这里是”非”的意思,不要和表示开头的”^”混淆。那怎么区分呢?很简单,表示开始位置的”^”只能用在正则表达式的最前端,而表示取反的”^”只用在字符组中,即只在中括号中出现。记住这一点,就不会搞混了。

日常工作中反义用得不多,因为扩大了范围。例如程序里的变量,第一个字符不允许是数字,一般使用"^[a-zA-Z_]"表示,而不会使用"\D",因为"\D"扩大了范围,包括所有非数字的字符,显然,变量命名不仅仅要求第一个字符不是数字,也不能是其他除了26个大小写字母和下画线以外的字符。因此,不要随意使用反义,以免无形中扩大范围,而使自己没有考虑到。

分支

分支就是存在多种可能的匹配情况。例如,匹配“cat”或者“hat”,可以写成[ch]at;要匹配“cat”、“hat”、“fat”、“toat”,很显然不能用字符组匹配的方式。这里表明前面的匹配字符可以是c、h、f或者to,而[]只能匹配单个字符,此时可用分支形式,即:

(c|h|f|to)at

其中括号里的表达式将视作一个整体(后面会讲到分组的概念),“|”表示分支,即可能存在的多种情况,可以匹配多个字符。分支的功能更强大,字符组方式只能对单个字符“分支”,而分支可以是多个字符以及更复杂的表达式。但对于单字符的情况,字符组的效率更高。也就是说,能使用字符组就不用分支。

注:上述的[ch]at 可以写成(c|h)at

正则表达式分支条件指有几种规则,无论满足其中哪一种规则都能匹配,具体方法是用“|”把不同规则分隔开,例如:

0\d{2}-\d{8}|0\d{3}-\d{7}

这个表达式能匹配以”-“分隔的电话号码:一种是3位区号,8位本地号码(如010-11223344),一种是4位区号,7位本地号码(如0312-1223454)。匹配3位区号的电话号码表达式如下:

\(0\d{2}\)[- ]?\d{8}|0\d{2}[- ]?\d{8}

其中区号可以用小括号括起来,也可以不用,区号与本地号间可以用”-“或空格隔开,也可以没有间隔。

例如,美国邮编规则是5位数字,或者用连字号间隔的9位数字。匹配表达式如下所示:

\d{5}-\d{4}|\d{5}

另外,使用分支条件时,要注意各个条件的顺序。如果改成如下形式,就只匹配5位邮编以及9位邮编的前5位。

\d{5}|\d{5}-\d{4}

注意:匹配分支条件时,将从左到右测试每个条件,如果满足某个分支,就不会再考虑其他条件了

分组

重复单个字符只需要直接在字符后面加上限定符,但如果想重复多个字符又该怎么办呢?可以用小括号指定子表达式,然后规定这个子表达式的重复次数,也可以对子表达式进行其他一些操作。常用分组语法如表所示。

示例

例如:简单的IP地址匹配表达式如下:

(\d{1,3}\.){3}\d{1,3}

要理解上述表达式,应按照如下顺序分析:

1)匹配1-3位的数字:

\d{1,3}

2)匹配3位数字机上1个英文句号(分组),重复3次(最后加上一个1-3位的数字):

(\d{1,3}\d){3}

IP地址中每个数字都不能大于255,所以严格的说这个正则是有问题的。如果使用算术比较,或许能简单地解决这个问题,但是正则表达式中没有提供关于数学的任何功能,所以只能使用冗长的分组、选择、字符类来描述一个正确的IP地址,如下所示:

((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)

解析:

2[0-4]\d|25[0-5]|[01]?\d\d?

上述就能表示0-255,因为2[0-4]\d 表示的为200-249,25[0-5] 表示的是250-255,[01]?\d\d? 表示的为0-199。

默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组,其组号为1,第二个为2,以此类推;分组0对应整个正则表达式。

也可以自己指定子表达式的组名,语法如下:

?<Word>\w+

把尖括号换成单引号也行,如下所示:

'Word'\w+

这样就把\w+组名指定为Word。

提示:组号分配远没有这么简单。组号分配过程是要从左向右扫描两遍:第一遍只给未命名组分配,第二遍只给命名组分配。因此,所有命名组的组号都大于未命名的组号。可以使用语法(?:exp)剥夺一个分组对组号分配的参与权。

反向引用

反向引用用于重复搜索前面某个分组匹配的文本。例如,”\1”代表分组1匹配的文本:

\b(\w+)\b\s+\1\b

以上表达式可以匹配重复的单词,例如go go或者kitty kitty。首先这个表达式是一个单词,也就是单词开始处和结束处之间大于一个的字母或数字,即”\b(\w+)\b”,这个单词会被捕获到编号为1的分组中,然后是1个或几个空白符(\s+),最后是分组1中捕获的内容(也就是前面匹配的那个单词),即\1,这样就相当于把匹配的重复一次。

要反向引用分组捕获的内容,可以使用"\k<Word>" ,所以上个例子可以写成这样:

\b(?<Word>\w+)\b\s+\k<Word>\b

例如,要捕获字符串"This is a 'string'" 引号内的字符,如果使用一下正则表达式:

(\"|').*?(\"|')

示例

将返回"This is a ' 。显然,这并不是我们想要的内容。这个表达式从第一个双引号开始匹配,遇到单引号之后就错误地结束匹配。这是因为表达式里包含”|”,也就是双引号(”)和单引号(’)均可。要修正这个问题可以用反向引用。

表达式“\1,\2,……,\9”是对前面已捕获子内容的编号,可以作为对这些编组的“指针”引用。在此例中,第一个匹配的引号就由1代表。可以这么写成:

("|\').*?\1

示例

如果使用命名捕获组,可以写成:

(?P<quote>"|').*?(?P=quote)

看PHP使用反向引用的例子。

在很多论坛中都会看到UBB标签代码。UBB标签最早的设计是用来在论坛和留言本里代替HTML,实现一些简单的HTML效果,同时防止滥用HTML出现安全问题。例如,HTML中粗体的标签是:

<b>粗体</b>

或者

<strong>粗体</strong>

而UBB标签则是:

[b]粗体[/b]

UBB标签以其更好的安全性,目前已经成为论坛发帖的代码标准,只不过不同论坛产品的叫法不一样而已。

最终,UBB标签还是要解析成HTML代码,才能让浏览器认识。这个过程是怎样实现的呢?下面以URL标签为例解释。

例如,UBB标签[url]1.gif[/url]用于插入表情。在解析时,需要把1.gif换成实际路径,并且需要用HTML的IMG标签进行替换,方法如下所示:

<?php
$str='[url]1.gif[/url][url]2.gif[/url][url]3.gif[/url]';
$s=preg_replace("#\[url\](?<WORD>\d\.gif)\[\/url\]#","<img src=http://www.test.com/upload/$1>",$str);
var_dump($s);

运行结果为:

string(126) "<img src=http://www.test.com/upload/1.gif><img src=http://www.test.com/upload/2.gif><img src=http://www.test.com/upload/3.gif>"

这里再给出一个表达式实现同样的效果:

<?php
$str='[url]1.gif[/url][url]2.gif[/url][url]3.gif[/url]';
$s=preg_replace("#\[url\](.*?)\[\/url\]#","<img src=http://www.test.com/upload/$1>",$str);
var_dump($s);

环视

断言用来声明一个应该为真的事实。正则表达式中,只有当断言为真时才会继续进行匹配。断言匹配的是一个事实,而不是内容。本文将介绍四个断言,它们用于查找在某些内容(但并不包括这些内容)之前或之后,也就是一个位置(如\b、^、)应该满足的一定条件(即断言),因此也称为零宽断言。

1.顺序肯定环视(?=exp)

零宽度正预测先行断言,又称顺序肯定环视,断言自身出现位置的后面能匹配表达式exp。

比如,匹配以“ing”结尾的单词前面部分(除了“ing”以外的部分):

\b\w+(?=ing\b)

示例

2.逆序肯定环视(?<=exp)

零宽度正回顾后发断言,又称逆序肯定环视,断言自身出现位置的前面能匹配表达式exp。

比如,以re开头的单词的后半部分(除了re以外的部分):

(?<=\bre)\w+\b
<?php
$reg = "#(?<=\bre)\w+\b#";
$str = 'reading a book';
preg_match_all($reg,$str,$m);
print_r($m);

运行结果为:

Array ( [0] => Array ( [0] => ading ) )

下面这个例子,同时使用上面这两种断言,匹配以空白符间隔的数字(再次强调,不包括这些空白符):

(?<=\s\d+(?=\s))

前面提到过反义,用来查找不是某个字符或不在某个字符类里的字符。如果只是想要确保某个字符没有出现,但并不想去匹配它时怎么办?例如,如果想查找这样的单词——出现字母q,但是q后面跟的不是字母u。可以尝试这样:

\b\w*q[^u]\w*\b

以上表达式匹配包含后面不是字母u的字母q的单词。但是如果多做几次测试就会发现,如果q出现在单词的结尾,例如Iraq、Benq,这个表达式就会出错。这是因为[^u]总要匹配一个字符,如果q是单词的最后一个字符,后面的“[^u]”将会匹配q后面的单词分隔符(可能是空格、句号或其他),后面的“\w*\b”将会匹配下一个单词,于是以上表达式就能匹配整个Iraq fighting。

逆序肯定环视能解决这样的问题,因为它只匹配一个位置,并不消费任何字符。现在,解决这个问题如下所示:

\b\w*q(?!u)\w*\b
<?php
$reg = "#\b\w*q[^u]\w*\b#";
$str = 'Iraq fighting';
preg_match_all($reg,$str,$m);
print_r($m);

运行结果为:

Array ( [0] => Array ( [0] => Iraq fighting ) )

<?php
$reg = "#\b\w*q(?!u)\w*\b#";
$str = 'Iraq fighting';
preg_match_all($reg,$str,$m);
print_r($m);

运行结果为:

Array ( [0] => Array ( [0] => Iraq ) )

3.顺序否定环视(?!exp)

零宽度负预测先行断言,又称顺序否定环视,断言此位置的后面不能匹配表达式“exp”。例如:
1)匹配3位数字,而且这3位数字的后面不能是数字:

\d{3}(?!\d)

2)匹配不包含连续字符串abc的单词:

\b((?!abc)\w)+\b

如果匹配的单词是c开头、t结尾,中间有一个字符,但不能是u(也就是说,整个单词不能是cut),直接用“c[^u]t”就可以了,若中间的字符不能是a或u(也就是说,整个单词不能是cat或cut),则表达式改为“c[^au]t”。

这个表达式能匹配的只是cot之类的单词,因为中间的排除型字符组“[^au]”必须匹配一个字符。可是,如果还想匹配chart、conduct和court怎么办?最简单的想法是:去掉排除型字符组的长度限制,改成“c[^au]+t”。

不幸的是,这样行不通,因为这个表达式的意思是:c和t之间由多于一个“除a或u之外的字符”构成,而chart、conduct和court都包含a或u。

我们发现,其实要否定的是“单个出现的a或u”,而不仅仅是“出现的a或u”,所以才出现这样的问题。要解决这个问题,就应当把意思准确表达出来,变成“在结尾的t之前,不允许只出现一个a或u”。想到这一步,就可以用顺序否定环视(?!……)来解决。表示在这个位置向右,不允许出现子表达式能够匹配的文本,把子表达式规定为“[au]t\b”(最后的“\b”很重要,它出现在t之后,保证t是单词的结尾字母)。有了限制,匹配a和t之间文本的表达式就随意很多,可以用匹配单词字符的简记法“\w”表示,于是整个表达式变成:

c(?![au]t\b)\w+t

注意:这里出现的并不是排除型字符组[^au],而是普通的字符组[au],因为顺序否定环视本身已经表示了否定。

进一步思考,整个匹配文本中都不能出现字符串“cat”,要怎么办呢?这个正则表达式应该是:

^(?:(?!cat).)+$

即在文本中的任意位置,都不能出现该字符串。

4.逆序否定环视(?

(?<![a-z])\d{7}

分析以下表达式,匹配不包含属性的简单HTML标签内的内容:

(?<=<(\w+)>).*(?=<\/\1>)

以上表达式最能表现零宽断言的真正用途。(<?(\w+)>)指定前缀为:被尖括号括起来的单词(比如可能是“<b>”),然后是“.*”(任意的字符串)最后是一个后缀(?=<\/\1>)。【注意】后缀里的“\/”,用到了前面提过的字符转义;“\1”则是反向引用,引用的正是捕获的第一组,即前面(\w+)匹配的内容,如果前缀实际上是“<b>”,后缀就是“</b>”。整个表达式匹配的是“<b>”和“</b>”之间的内容(再次提醒,不包括前缀和后缀本身)。

总体而言,环视相当于对“所在位置”附加一个条件,难点就在于找到这个“位置”。这一点解决了,环视就没有什么秘密可言了。

贪婪/懒惰匹配模式

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。例如以下表达式将匹配以a开始,以b结束的最长字符串:

a.*b

如果用来搜索“aabab”,它会匹配整个字符串“aabab”。这就是贪婪匹配。

有时,需要匹配尽可能少的字符,也就是懒惰匹配。前面给出的限定符都可以转化为懒惰匹配模式,只要在后面加上一个问号。例如“.*?”就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。例如,匹配以a开始、以b结束的最短字符串,正则表达式如下:

a.*?b

把上述表达式应用于aabab,如果只考虑“.*?”这个表达式,最先会匹配到aab(1~3字符)和ab(第2~3个字符)这两组字符。

为什么第一个匹配是aab(第1~3个字符)而不是ab(第2~3个字符)?简单地说,正则表达式有另一条规则,比懒惰/贪婪规则的优先级更高:最先开始的匹配拥有最高优先权。

常用懒惰限定符如表所示。

示例

懒惰模式匹配原理简单来说,是在匹配和不匹配都可以的情况下,优先不匹配,记录备选状态,并将匹配控制交给正则表达式的下一个匹配字符。当后面的匹配失败时,回溯,进行匹配。关于回溯以及正则表达式效率等高级内容,可以查阅《精通正则表达式》一书。

例如:

<?php
$str='[url]1.gif[/url][url]2.gif[/url][url]3.gif[/url]';
$s=preg_replace("#\[url\](.*?)\[\/url\]#","<img src=http://www.test.com/upload/$1>",$str);
var_dump($s);

运行结果为:

string(126) "<img src=http://www.test.com/upload/1.gif><img src=http://www.test.com/upload/2.gif><img src=http://www.test.com/upload/3.gif>"

如果改为贪婪匹配

<?php
$str='[url]1.gif[/url][url]2.gif[/url][url]3.gif[/url]';
$s=preg_replace("#\[url\](.*)\[\/url\]#","<img src=http://www.test.com/upload/$1>",$str);
var_dump($s);

运行结果为:

string(74) "<img src=http://www.test.com/upload/1.gif[/url][url]2.gif[/url][url]3.gif>"

在贪婪模式下,由于匹配表达式是“.*”,即任意字符出现任意次,这个正则表达式会一直匹配[url]后的内容,直到遇到结束条件“[\/”。

实际开发中,涉及贪婪模式与懒惰模式的地方是很多的。在一定情况下,使用懒惰模式可以减少回溯,提高效率。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页