4.5 正则表达式
正则表达式是一种模糊匹配模式,特别适合于模糊查找与替换。很多高级语言都逐渐支持正则表达式。正则表达式在历史上出现过两种比较流行的语法:POSIX和Perl,但是由于Perl的效率更高,因此PHP自5.3.0版本起仅支持Perl兼容的正则表达式。
4.5.1 正则表达式的基本知识
1.正则表达式的定义
正则表达式是由普通字符(如字符a~z)和特殊字符组成的字符串模式。该模式设定了一些规则,当正则表达式函数使用这些规则时,可以根据设定好的内容对指定的字符串进行匹配。使用正则表达式可以完成以下功能。
1)测试字符串的某个模式。例如,可以对一个输入字符串进行测试,看在该字符串中是否存在一个E-mail地址模式或一个身份证模式,这称为数据有效性验证。
2)替换文本。可以在文档中使用一个正则表达式来标志特定字符串,然后全部将其删除,或者替换为别的字符串。
3)根据模式匹配从字符串中提取一个子字符串。可以用来在文本或输入字段中查找特定字符串。
正则表达式是由普通字符、特殊字符组成的一种字符模式,它由两个斜杠(/)括住。
2.正则表达式的特殊字符
(1)行定位符(^与$)
行定位符是用来描述字符串的边界。“$”表示行结尾,“^”表示行开始。如"^de"表示以de开头的字符串;"de$",表示以de结尾的字符串。
(2)单词定界符
在查找的一个单词的时候,如an是否在一个字符串“gril and body”中存在,很明显如果匹配的话,an肯定是可以匹配字符串“gril and body”匹配到,怎样才能让其匹配单词,而不是单词的一部分呢?这时候,可以是用单词定界符\b。
\ban\b去匹配“gril and body”的话,就会提示匹配不到。
当然还有一个大写的\B,它的意思,和\b正好相反,它匹配的字符串不能使一个完整的单词,而是其他单词或字符串中的一部分。如\Ban\B。
(3)选择字符(|),表示或
选择字符表示或的意思。如Aa|aA,表示Aa或者是aA的意思。注意使用“[]”与“|”的区别,在于“[]”只能匹配单个字符,而“|”可以匹配任意长度的字符串。在使用“[]”的时候,往往配合连接字符“-”一起使用,如[a-d],代表a或b或c或d。
(4)排除字符,排除操作
正则表达式提供了“^”来表示排除不符合的字符,^一般放在[]中。如[^1-5],该字符不是1~5之间的数字。
(5)限定字符(?*+{n,m})
限定字符主要是用来限定每个字符串出现的次数。如表4-3所示。
表4-3 限定字符
如,(D+)表示一个或多个D。
(6)点号操作符
匹配任意一个字符(不包含换行符)。
(7)表达式中的反斜杠(\)
表达式中的反斜杠有多重意义,如转义、指定预定义的字符集(如表4-4所示)、定义断言、显示不打印的字符(如表4-5所示)。
表4-4 指定预定义的字符集
表4-5 显示不可打印的字符
(8)转义字符
转义字符主要是将一些特殊字符转为普通字符。而这些常用特殊字符有“.”“?”“\”等。
(9)括号字符()
在正则表达式中,小括号()的作用主要有:
改变限定符如(|、*、^)的作用范围。如(my|your)baby,如果没有“()”,|将匹配的是要么是my,要么是yourbaby,有了小括号,匹配的就是mybaby或yourbaby。
(10)反向引用
反向引用,就是依靠子表达式的“记忆”功能,匹配连续出现的字串或是字符。如(dqs)(pps)\1\2,表示匹配字符串dqsppsdqspps。
(11)模式修饰符
模式修饰符的作用是设定模式,也就是正则表达式如何解释。PHP中主要模式修饰符如表4-6所示:
表4-6 模式修饰符
3.正则表达式的特殊字符应用
1)身份证号码由18位数字或17位数字后加一个X或Y组成,因此,身份证号码的正则表达式为:/[0-9]{17}[0-9XY]/。
2)邮政编码由6位数字组成,因此,邮政编码的正则表达式为:/[0-9l{6}/。
3)E-mail地址的正则表达式为:/[a-zA-Z0-9_\-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9_\.]+/。
其中,子表达式/[a-zA-Z0-9_\-]匹配E-mail用户名,由字母、数字、下画线和“-”组其中,子表达式/[a-zA-Z0-9_\-]匹配E-mail用户名,由字母、数字、下画线和“-”组成;子表达式[a-zA-Z0-9-]匹配主机的域名,由字母、数字和下画线组成;“\.”匹配点号(.);子表达式[a-zA-Z0-9_\.]+匹配域名的剩余部分,由字母、数字和下画线组成。
4)url地址的正则表达式为:
/^http(s?):\/\/(?:[A-za-z0-9-]+\.)+[A-za-z]{2,4}(:\d+)?(?:[\/\?#][\/=\?%\-&~'@[\]\':+!\.#\w]*)?/。
5)手机号码的验证的正则表达式为:/1[345678]\d10/。
4.5.2 正则表达式在PHP中的应用
1.字符串匹配
所谓的字符串匹配,言外之意就是判断一个字符串中,是否包含或是等于另一个字符串。如果不使用正则匹配,可以使用PHP中提供了很多方法进行这样的判断。
(1)不使用正则匹配(使用字符串函数)
1)strstr()函数。
● haystack是当事字符串,needle是被查找的字符串。该函数区分大小写。
● 返回值是从needle开始到最后。
● 关于$needle,如果不是字符串,被当作整形来作为字符的序号来使用。
● before_needle若为true,则返回前东西。
Stristr()函数与strstr()函数相同,只是它不区分大小写。
2)strop()函数。
● 可选的offset参数可以用来指定从haystack中的哪一个字符开始查找。返回的数字位置是相对于haystack的起始位置而言的。
● stripos—查找字符串首次出现的位置(不区分大小定)。
● strrpos—计算指定字符串在目标字符串中最后一次出现的位置。
● strripos—计算指定字符串在目标字符串中最后一次出现的位置(不区分大小写)。
(2)使用正则进行匹配
在PHP中,提供了preg_math()函数和preg_match_all()函数进行正则匹配。关于这两个函数的原型如下:
函数功能为:搜索subject与pattern给定的正则表达式的一个匹配。
● pattern:要搜索的模式,字符串类型。
● subject:输入字符串。
● matches:如果提供了参数matches,它将被填充为搜索结果。matches[0]将包含完整模式匹配到的文本,matches[1]将包含第一个捕获子组匹配到的文本,以此类推。
● flags:flags可以被设置为标记值:PREG_OFFSET_CAPTURE。如果传递了这个标记,对于每一个出现的匹配返回时会附加字符串偏移量(相对于目标字符串的)。注意:这会改变填充到matches参数的数组,使其每个元素成为一个由第0个元素是匹配到的字符串,第1个元素是该匹配字符串在目标字符串subject中的偏移量。
● offset:通常,搜索从目标字符串的开始位置开始。可选参数offset用于指定从目标字符串的某个未知开始搜索(单位是字节)。
● 返回值:preg_match()函数返回pattern的匹配次数。它的值将是0次(不匹配)或1次,因为preg_match()函数在第一次匹配后将会停止搜索。preg_match_all()函数不同于此,它会一直搜索subject直到到达结尾。如果发生错误preg_match()函数返回false。
【例4-13】判断字符串“http://www.baidu.com”中是否包含baidu?
方法一:不使用正则表达式
如果不使用正则表达式,使用strstr()函数或者strpos()函数中任意一个都可以,在此,将使用strstr()函数,代码如下:
方法二:使用正则表达式
只需要判断字符串是否存在即可,所以选择preg_match()函数。
【例4-14】单词定界符判断:判断字符串“I am a good boy”中是否包含单词go。
首先判断是单词,而不是go这两个字母的字符串,因此比较的时候,需要比较是否包含“go”,即在字符串go前后有一个空格。
如果使用非正则比较,只需要调用上面的checkStr1()函数即可,注意,第二个参数前后要加一个空格,即‘go’。如果使用正则表达式,可以考虑使用单词定界符\b,即:$pattern=‘/\bgo\b/’;,然后调用checkStr2()函数即可。
【例4-15】反向引用:判断字符串“I am a good boy”中是否包含3个相同的字母。
此时,如果不使用正则,将会很难判断,因为字母太多了,不可能去将所有字母分别与该字符串比较,那样工作量也比较大。这时候涉及到了正则表达式的反向引用。在PHP正则表达式中,通过\n,来表示第n次匹配到的结果。如\5代表第五次匹配到的结果。那么本例的正则表达式为:$pattern='/(\w).*\1.*\1/';。
需要注意的是,在使用反向匹配的时候都需要使用小括号(),匹配()里面出现的字符或字符串。
2.字符串替换应用
(1)不使用正则匹配(使用字符串函数)
在PHP中当替换字符串的时候,通常可以使用str_replace()、substr_replace()等字符串函数,这两个函数的区别如表4-7所示。
表4-7 str_replace()函数、substr_replace()函数的区别
【例4-16】将字符串“hello,中国”中的hello替换为'你好'。
如果不使用正则表达式匹配,而使用字符函数,为:
或是:
(2)使用正则匹配
如果使用正则替换,PHP中提供了preg_replace_callback()函数和preg_replace()函数,preg_replace()函数原型如下:
函数功能描述:在字符串subject中,查找pattern,然后使用replacement去替换,如果有limit则代表限制替换limit次。Preg_replace_callback()函数与preg_replace()函数功能相似,不同的是preg_replaceback()函数使用一个回调函数callback来代替replacement。
使用正则匹配,上边例4-15实现语句如下:
3.字符串分割应用
php提供了explode()函数去分割字符串,与其对应的是implode()函数。关于explode()函数原型如下:
关于通过正则表达式进行字符串分割,PHP提供了split()函数、preg_split()函数。preg_split()函数通常是比split()函数更快的替代方案。preg_split()函数原型如下:
【例4-17】将字符串“http://product.dangdang.com/23882990.html”按照'/'进行分割。
方法一:
方法二:
4.5.3 正则表达式在JavaScript中的应用
test()函数是JavaScript提供的最重要的正则表达式函数,用于验证用户输入的数据是否满足指定的格式,该函数的语法格式如下:
说明:在字符串中查找与正则表达式相匹配的内容,若找到,则返回true;否则返回false。若正则表达式未含“^”或“$”,只要正则表达式为字符串的子串,该函数就返回true。若正则表达式包含“^”或“$”,只有正则表达式与字符串完全匹配,该函数才返回true。
【例3-18】 test()函数的用法示例。
4.5.4 正则表达式的其他特性
1.贪婪匹配与惰性匹配特性
(1)贪婪匹配:匹配尽可能多的字符
比如,正则表达式中m.*n,它将匹配最长以m开始,n结尾的字符串。如果用它来搜索manfakjkakn的话,它将匹配到的字符串是manfakjkakn而非man。可以这样想,当匹配到m的时候,它将从后面往前匹配字符n。
(2)惰性匹配:匹配尽可能少的字符
有的时候,我们需要并不是去贪婪匹配,而是尽可能少的去匹配。这时候,就需要将其转为惰性匹配。怎样将一个贪婪匹配转为惰性匹配呢?只需要在其后面添加一个“?”即可。如m.*?n将匹配manfakjkakn,匹配到的字符串是man。惰性匹配的字符描述如表4-8所示。
表4-8 惰性字符描述
2.PHP正则表达式之回溯与固态分组
(1)回溯
首先需要清楚什么是回溯,回溯就像是在走岔路口,当遇到岔路的时候就先在每个路口做一个标记。如果走了死路,就可以照原路返回,直到遇见之前所做过的标记,标记着还未尝试过的道路。如果另条路也走不了,可以继续返回,找到下一个标记,如此重复,直到找到出路,或者直到完成所有没有尝试过的路。
比如:
看到上面的程序,可能都清楚是什么意思,就是匹配$str是否包含这样一个由“a+0个或多个字母+c”不区分大小写的字符串。
(2)固态分组
固态分组,目的就是减少回溯次数,使用(?>…)括号中的匹配时如果产生了备选状态,一旦离开括号便会被立即抛弃掉。举个典型的例子如:‘\w+:’这个表达式在进行匹配时的流程是,会优先去匹配所有的符合\w的字符,假如字符串的末尾没有‘:’,即匹配没有找到冒号,此时触发回溯机制,该机制会迫使前面的\w+释放字符,并且在交还的字符中重新尝试与‘:’作比对。但是问题出现在这里:\w是不包含冒号的,显然无论如何都不会匹配成功,可是依照回溯机制,引擎还是继续往前找,这就是对资源的浪费。所以就需要避免这种回溯,方法就是将前面匹配到的内容固化,不令其存储备用状态,那么引擎就会因为没有备用状态可用而只得结束匹配过程。大大减少回溯的次数。
如下边这段代码,就不会进行回溯:
当然有的时候,又需慎用固态分组,如下边这段代码,要检查$str中是否包含以a结尾的字符串,很明显字符串中是包含字母a的,但是因为使用了固态分组,反而达不到想要的效果。
注:PHP中正则表达式在某些时候,能帮解决PHP函数很多困难的匹配或是替换。然而PHP中正则表达式的效率问题,是必须要考虑的,在某些时候,能不用正则表达式还是尽量不去用它,除非某些场合必须用到,或是我们能够有效减少其回溯次数。