在正则表达式的使用上,俺一直在打游击,是时候来个总结了。由于愚笨等不可抗拒的原因,只能选简单的说,只写下对ASCII编码字符的匹配和php相关的,其他的Unicode的和其他不同流派的,以后碰到了再学习下就行,应付平常的应该够了。
首先需要接受正则的概念,它是用来寻找文本的一种规则,简单的文本检索,比如字符串查找strpos,只是简单的查找某一串字符的出现,只要稍微变动下需求,比如要查找这样5个字符,前面两个是数字,后面3个是字母,且忽略大小写,如果单纯用字符串查找等方法做,显得很麻烦,而用正则表达式就无比简单了。正则很强大很灵活,当然灵活的东西一般规则也有点多。在平常生活中,不知不觉会用到正则类似的东西,比如搜F盘下的所有文本文件:*.txt,或者在某些编辑器如Sublime Text,对正则超找提供了支持,又或者在linux命令中也经常用到:find . -name '*.log' -print查找当前目录下扩展名为log的文件,或者查询日志时cat 2015-10-18-log |grep 'show_list'(这个不是,只能说类似):打印出记录了show_list接口的行等等,以及Apache的Rewrite Module对访问链接的控制,这些都类似或者就是正则表达式的应用。
在php中,用来支持正则的有三种引擎:preg、ereg、mb_ereg,所谓引擎简单理解就是底层一些对php在进行正则匹配查找时进行支持的库和接口,不同的库有不同的名字,现在用的多是preg,它在功能和速度上要强于其余两者。preg引擎(或者说套件),即“Perl的正则表达式”(Perl Regular Expression),源于某大牛对当时的ereg套件在性能上的不满,想做一个更好的库,于是查阅Perl处理正则的源代码,但是在他之前另外一大牛,也遇到过这个问题,此大牛(后者)研究了一下Perl正则源代码,觉得繁琐复杂,于是自己写了一套兼容Perl的正则库PCRE(Perl Compatible Regular Expression),编写清晰,效率出众,文档完备,然后前一位大牛将其改写到php中,就成了preg,一直慢慢改进到今天。所以preg兼容PCRE,PCRE兼容Perl的正则,preg就是Perl正则的亲戚,以上为历史。
一个简单的正则表达式示例:'/abc/',表示匹配一个字符串中的abc三个字符,完全等价于普通的字符串查找方法,正则就是一个字符串,只是特殊的符号有特殊的含义而已,而一般将这串表示正则的字符串称为:模式(pattern)。
1. 定界符
在定义一个正则表达式时,首先要有定界符,表示表达式从这儿开始,到那儿结束,最常用的是/,如匹配abc的'/abc/',从a开始,到c结束。在php中,还可用其他的,比如#、!、{}(左边用{,右边用})等,全凭个人习惯。
2. 原子
原子是正则表达式中最基本的单位,细分为5类,俺将伴随它的功能来记录。
首先,可以用最普通的字符作为原子,如a、B、c、1、2、_等
'/9527/' '/misson failed/' '/PHP_VERSION/'
一些特殊字符和元字符(metacharacter)也可以,任何一种符号都能在模式中使用,前提是别跟它自己的表示特殊含义的符号相冲突,前面说过,正则很灵活很强大,强大在于有这些特殊含义符号的辅助(有什么特殊作用后面再说),如果正则中要匹配这个在这里有特殊含义的符号,需要转义(escape),很好理解,就跟双引号字符串中有双引号也要转义一样。这些特殊符号包括.、*、?、+、‘、"、\ 、/ 等等,其实单引号、双引号有时不必转,全看你写的表达式用的是单引号字符串还是双引号字符串,在php中为避免出错写正则一般用单引号字符串。还有就是别跟自己的定界符冲突,否则系统以为表达式提前结束而出错。
'/a\.b\?c\+/' '/ab\/123/'
非打印字符作原子,非打印字符泛指空白字符,即空格、水平制表符\t、垂直制表符\v、回车符\r、换行符\n等,注意此处易埋bug,php用单引号字符串,减少出错
'/\r\n/'
前面说过,可以匹配一般的单个字符,如9527,9后面接着一个5,再接着一个2,然后是7,现在匹配的并不限于这几个数字,比如我要匹配两个数字,只要是数字就行,对于这种带有通用性质的字符,正则也有表示某一类字符的的表示形式,比如表示数字用\d,表示一个0到9的十进制数字,而\D(大写形式)表示非十进制数字,\w表示匹配一个单词,在php的正则流派里边,单词的定义是大写字母A-Z,小写字母a-z,数字0-9以及下划线_,相应的\W大写形式表示相反的意义。
'/\d\d/' // 匹配两个数字 '/\D/' // 匹配一个除数字之外的任意字符 '/\w/' // 匹配一个词 '/\W/' // 匹配一个除了词的任意字符 '/\s/' // 匹配一个空白字符 '/\S/' // 匹配一个非空白字符
3.元字符
元字符就是有特殊意义的字符,如*、+、?、. 、|、^、$等,一般不能单独出现,只有在修饰它前面的一个(或一些)原子时才表现出它自己的特殊含义,也就是说,它要配合上面说的原子使用才有意义。它有特殊意义,但就是要匹配它时,就得使用转义符\了,转为普通字符。
?:量词,有或者没有
现在要匹配一个单词意思颜色,有两种写法,colour或color,中间的u要么有1个,要么没有,元字符?就适合,而且,如果没有加括号限制,元字符量词这类元字符只对位于它前面一个原子起作用,这里?只作用于u
'/colou?r/'
+:量词,一个或多个
比如匹配一个或多个数字,注意元字符修饰的原子有可能是个序列,但未加括号限制只对一个有效
'/\d+/'
*:量词,0个、1个或多个,即任意数量
'/\d*/'
区间:规定重复出现次数
前面的次数限制毕竟比较死板,来个活的,区间用一对{}表示,{n}表示出现n次,{n,}表示大于或等于n次,{m,n}表示至少出现m次,至多出现n次,最好m小于n,别故意为难系统~
'/auth{0,1}/' // 出现0次到1次,即? '/auth{1,}/' // 至少出现1次,即+ '/auth{3}/' // h要出现3次
匹配任意字符
.,点号,匹配任意字符,在php中,默认情况下除了换行符,它可以匹配任意任意一一个字符,另外一种情况下,它就真的匹配任意字符,包括换行符,后面再说。
多选结构
|,表示或,依靠它可以生成一个多选分支,需要注意的是|在正则中优先级最低,,下面的不是|作用的不是左右的r和c,而是|左右的子表达式,哪怕把一个表达式写成'/\d+\s*abc{2,5}|ack?\d/',它仍作用于它前面和后面的子表达式\d+\s*abc{2,5}和ack?\d,只要没有加括号限制
'/color|colour/' // 匹配color或colour
字符组
匹配若干字符之一,现在要匹配abc中任一个字符,可以这样:[abc],包在一堆中括号里边,加入要匹配大写A到大写Z任一个字符,可以这样:[A-Z],中间一个连字符在字符组里边就成了有特殊意义的元字符表示从什么到什么,,其他还有数字[0-9],或者只是数字、字母的部分值:[2-5]、[c-h],有几个需要注意的地方:
1. 连字符-只有在字符组内部,且在两个字符之间才被认为是有效的元字符,出了字符组跟a一样是普通字符;
2. 如果字符组内部确实需要匹配-,最好将它放在字符组内最前面,如[-a-z],匹配小写字母或-,在php中也可以将它放在最后,如[c-k-](居然没报错-_-#),但放在最前面还是最保险;
3. 对于系统不认可的由-标识的顺序字符,对于php会报Compilation failed警告,如[a-9]、[9-0]
4. 字符组的顺序一般为从小到大,不能[9-0],大小写一般分开写[A-Za-z],但在php中,至少俺这个5.5版本可以大小写混合写[A-z],表示匹配从A到Z和a到z的字母,不提倡这样做,其他语言流派肯能不支持
'/C[EFIMT]O/' '/[A-Za-z0-9_]/' // 相当于\w,表示单词 '/[a-z]+/' // 匹配一个或多个小写字母
排除型字符组
前面是匹配任意一个[]内的字符,现在恰好相反,不匹配任意一个字符[]内的字符,如[^a-z]:不匹配任意一个小写字母,在字符组内最前面加^表示取反。一个有意思的例子:
'/abc[^ABC]/' 字符串1: 'abcK' 字符串2: 'abc' // 匹配?
字符串1很显然,字符串2匹配吗?注意字符组:它匹配未出现的字符。这里abc后面要跟一个除A、B或C的字符,但是不能没有,这便是字符组的坑。
单词边界
有时为了匹配一个完整单词,一般单词给人印象是左右带有空格,专门有一个表示单词的这种边界元字符\b,注意它并不匹配一个空格,而是一个位置,所以单独的'abc'也被看作一个单词,虽然它左右并没有空格,关于匹配位置后面再详说。但是正则中,至少目前对单词的定义还没那么强大,就是大小写字母、数字加下划线,跟单词边界相反的就是\B,只要不是单词边界都能匹配
'/\b\w+\b/' // 匹配一个单词 'hello world' // 匹配hello(只匹配一次的话) 'abc' // 匹配abc
括号限定
括号的第一个强大功能,是限定元字符的作用范围,将表达式分成一个个子表达式,如'/col(ou)?r/',加了括号后,?元字符作用的对象是ou而不再只是u,'/abc|def/'指的是匹配abc或者def,但a(bc|de)f匹配的是abcf或者adef,类似编程语言括号运算符,括号内是一个小的单元,相当于一个大原子,当然它还有一个重要作用:分组,后面再说。
行的开头与结尾
每一行字符串都有一个开头和结尾,开头和结尾指一个位置,虽然我们在描述一串字符串会说以...结尾,但正则中不指具体字符,匹配的是位置。比如'/^a.+/',匹配以a开头的字符串,^表示开头,$表示结尾,下面是几个值得注意的例子
'/^/' // 匹配一个行开头,只要是一行字符串都有开头,哪怕空字符串,无实际意义 '/^$/' // 匹配一个行开头,紧接着是行结尾,即匹配一个空行 '/^hello$/' // 匹配以hello开头的字符串,随后改行便结尾,即匹配只有hello字符串的行,该行再无其他字符 '/^hello.*hello$/' // 匹配一个行开头,接着是hello,接着可能是若干其他字符,然后是hello,紧接着是行结尾,即匹配一个一hello开头,hello结尾,中间有若干字符的字符串
注意在默认情况下,脱字符^和美元符号$表示匹配一个字符串的开头和结尾,比如"hello",结尾在紧挨着o的右边,那么"hello\nabc"(在php的双引号字符串中,\n是元序列,表示换行符,但php的单引号字符串\n只是普通字符,一个\一个n,特此说明)该看做一行还是两行呢?答案是,在preg中,默认情况下,仍被看做一行,它的结尾是紧挨着c的右边,可以验证下
<?php $pattern = '/hello$/'; // 以hello结尾 $subject = "hello\nabc"; // 匹配字符串的结尾,而不是逻辑行的结尾 preg_match($pattern, $subject, $match); echo 'match=><pre>'; var_dump($match); // 无匹配内容
我们把"abc\n"称为一个逻辑行,把"abc\ndef\n"称为有两个逻辑行,因为逻辑上它是有两行字符串的,但preg默认不匹配多个逻辑行(可能是坑),管你几个换行符就当一行字符串看,$直接匹配到这个字符串最后一个位置。那么php的正则能不能匹配多个逻辑行呢?当然行,要用到模式修饰符,后面再说。
除了以^表示行开头,$表示行结尾,在preg中,\A也表示开头,\z和\Z都表示结尾,不同的是\Z能匹配到最后的换行符,而\z不行。
3. 分组和捕获
一个简单的例子,匹配以单引号或双引号开头的字符串,结尾需要是对应的双引号或单引号(中间不存在转义过的双引号或单引号)
'/["\'].*["\']/'
如果是上面那样,第一个字符组匹配了双引号,是无法保证第二个字符组匹配相同的双引号,即现在的要求是,前面那个字符组匹配了什么,后面那个字符组也需要匹配相同的字符,不是给一个相同的正则表达式就行。
前面说过括号的第一大作用,限定某些元字符的作用范围,括号的另一大作用就是分组捕获,这是正则的特性。比如'/ab(cd)ef(gh)ij/',有两个括号,preg引擎会对每个括号中匹配到的文本进行记录,以左括号由左往右数,以数字编号,\1记录的是第一个左括号所在的里边的内容,\2记录第二个左括号所在的里边的内容,第n个左括号对应\n,preg最多可记录4096个(强大!),这里\1对应cd,\2对应gh。括号起到了分组的作用,而类似\1、\2...\n称为反向引用。注意:引用的是正则匹配到的文本,而不是引用的正则表达式。
$pattern = '/(\w)(\d)(.*)/'; // 匹配一个词,一个十进制数字和若干任意字符 $subject = 'a57h'; preg_match($pattern, $subject, $match); echo 'match=><pre>'; var_dump($match);
结果:
preg_match方法将捕获的文本放在$match参数中,数组索引的1、2、3对应的元素分别是捕获到的\1、\2、\3三个分组文本(索引0列出的是整个表达式匹配到的文本)。
对于括号嵌套的,只看左括号的相对顺序,比如'/(abc(def)g(hij))(k)/',有效的左括号为4个,\1对应abc的左括号整个括起来的内容(abcdefghij),也包括里边嵌套括号匹配的内容,\2对应def,\3对应hij,\4对应k。
所以对于上边引号对应的例子可以是这样的:'/(["\'].*)\1/',后面的字符要与第一个匹配到的文本一致(注意不是模式一致)。
但是新的变种又来了,有的想分组捕获,但有的只想分个组,限定下范围,不想捕获,?:来了,(?: ... ),在分组的括号最前面添加问号冒号,就表示取消文本捕获。因此对于'/(?:abc(de)fg(?:hij))/',\1捕获的是de,没有\2。
除了数字捕获(\后面跟数字来标识捕获内容),还可使用命名捕获,即给捕获到的文本取名字,用法(?P<name>...),对括号添加?P<name>,name是取的名字,有的书上写着(?P=name),5.5亲测不行。
$pattern = '/\w(?P<key1>\w\w)\s+(?P<key2>\d+)/'; $subject = 'abcd 233'; preg_match($pattern, $subject, $match); echo 'match=><pre>'; var_dump($match);
结果:
在php的命名捕获时,把原来数字形式的捕获也记录了,key1与\1对应,key2与\2对应,反正多一种不嫌多。
捕获的另一个巨大的用处,就是在替换操作时,对匹配的文本的引用,比如现在有表达式 '/123(\d\d\d)ok([a-z][a-z])end/',括号捕获到三个数字和两个小写字母,我们想提取出来,怎么办,就preg来说,使用简单的替换方法:preg_replace。一个例子
$pattern = '/123(\d\d\d)ok([a-z][a-z])end/'; $subject = '123233okhiend'; $replacement = '$1---$2'; // 在替换字符串中,对捕获到文本的引用 $ret = preg_replace($pattern, $replacement, $subject); echo 'ret=><pre>'; var_dump($ret);
括号捕捉到了233和hi,如果是反向引用我们知道是\1和\2,替换的字符串$replacement中,使用$1、$2来捕捉它,注意\1、\2是用在原始的正则表达式中的,这里是替换字符串中,当然,替换操作的是字符串的副本。
结果:
如果用命名捕获来操作,使用对应的$name来引用,亲测貌似不行,估计是怕跟上下文中的变量相冲突,但是上面说了,在命名捕获时,数字索引仍然是有效的,所以在命名捕获时,我们仍然可以再替换字符串中通过$1来引用匹配到的文本。
一个需要注意的问题:假设我捕获到了第1组,替换文本是'$15',1后边又跟着个数字,容易出错,为提高解析效率,可使用{}将数字括起来,如'${1}5',另,这对命名捕获仍无效。
4. 环视
前面的行开头、结尾是对位置的匹配,环视( lookaround)也是对位置的匹配,对于这种位置匹配,可能一个更熟悉的名字是:零宽断言。^、$、\A、\Z、\b、\B都算零宽断言,包括环视结构,当然行开头和结尾也多称为锚点。比如俺曾做的一个小项目中,有这样的需求,使用一个别人搭建的框架,每一个model类处理一张表,表名根据model类的类名确定,它的类名一般是这样的class externalLinks extends Model{...},子类名是这样externalLinks,它的表名是gw_external_links,gw_不说,从externalLinks到external_links需要在左边是小写字母,右边是大写字母的地方插一个_,再转小写,而且类名不一定只有这样的,比如abcDefGhi,可能是3个大小写单词,个数不确定,用字符串的替换显得比较麻烦,因为个数不确定,用正则轻而易举就解决了。凡是这种某地方的左边是啥,右边是啥,明显的位置要求的,都可以用环视试一下。环视分为四种:
肯定顺序环视:(?=...),匹配某个位置,它的右边是...
肯定逆序环视:(?<=...),匹配某个位置,它的左边是...
否定顺序环视:(?!...),匹配某个位置,它的右边不是...
否定逆序环视:(?<!...),匹配某个位置,它的左边不是...
不看...就是环视的符号定义,顺序、逆序的区别在于一个从左往右看,一个从右往左看。环视在位置检测时非常有用。例如现在要匹配abc,它的右边必须是数字,可以这样
'/abc(?=\d)/'
说说俺自己上面那个问题,找到左边是小写字母,右边是大写字母的位置,可以这样
'/(?=[A-Z])(?<=[a-z])/'
如果左右两边都需要检测的,像这种,顺序环视跟逆序环视的左右顺序的顺序不用管,哪个放左边哪个放右边都行(等价于'/(?<=[a-z])(?=[A-Z])/')。一般来说逆序环视放左边,顺序环视放右边,一个好习惯是以位置为中心,想象这个位置左边应该有什么,右边应该有什么。上面的模式找到位置了用_替换(实际是插入)掉问题基本就解决了。再比如
$pattern = '/\w+(?=\d)/'; $subject = 'abcd 233'; preg_match($pattern, $subject, $match); echo 'match=><pre>'; var_dump($match);
结果:
想想为啥?匹配一个或多个词,它的右边必须是数字,只有'23'符合,它的右边恰好为数字,2的前面是空格,不属于\w范围,而'233'的后面啥都没有。
注意:只匹配位置,不匹配实际文本,这个有时在多次匹配时有很大区别。
对于单词边界符\b,也可用环视写出来,单词的开始边界是左边非单词右边为单词:(?<!\w)(?=\w),单词的结束边界是左边为单词右边非单词:(?<=\w)(?!\w),合在一起就是:(?<!\w)(?=\w)|(?<=\w)(?!\w)(|,或的左右表达式是整体,它的优先级最低)。
5. 占有优先 vs 忽略优先
标准量词都是匹配优先的,如*、+、?以及区间量词{m,n},它们总是在自己的范围内尽可能多的匹配字符,对于{m,n}(m<n)有n个的话绝不止匹配m个就完事儿。例如
$pattern = '/\w+(?=\d)/'; $subject = 'hello123'; preg_match($pattern, $subject, $match); echo 'match=><pre>'; var_dump($match);
结果:
上例在匹配单词时总是尽可能匹配最多,所以单看\w+会一直匹配到字符串结尾3处,但随后的环视检查出现右边不是一个数字(什么都没有),于是交还一个词,再看看右边是不是数字,这下是了(hello12)然后就匹配成功返回,这里有一个回溯的过程(下一篇写写),不细纠结,有的书称这种匹配优先为贪婪模式,即标准量词默认都是贪婪的,尽可能多的去匹配的,那么与之对应的就有非贪婪模式。
在量词后面加个问好,表示非贪婪模式,即忽略优先,如*?、+?、??、{m,n}?,非贪婪模式下的量词总是尽可能匹配少的字符,还是上面那个例子,如果把$pattern改为下面呢?
$pattern = '/\w+?(?=\d)/'; // 忽略优先
匹配的结果是'hello',过程是首先+?使得\w每匹配一个词就检查下右边是不是数字,是的话匹配就此停止,返回结果,要保证匹配到的字符最好嘛,不是的话继续匹配,直到右边是数字便立即停止返回。忽略优先可以提前返回结果,当然可能不是最优的,提高匹配速度(下一篇再总结下)。
有木有倔强的不交还的列?当然有,这就是占有优先,它在标准量词后面加一个+,如*+、++、?+、{m,n}+,把上面的例子再改改,有意向不到的结果-_-#
$pattern = '/\w++(?=\d)/'; // 占有优先
记住占有优先的特点:不交还!\w+一直匹配到hello123结尾,发现之后啥都没有,失望至极,然后停止匹配,报告匹配失败了,没错这次居然是匹配失败了!占有优先仍属于匹配优先,尽可能多的去匹配,只是匹配到最后如果发现某条件不符,不好意思,他不服,直接停止。所以占有优先可以提前报告失败,提高匹配速度。
忽略下占有优先的脾气,来看一个新东西:固化分组。表示方式:(?>...)(它应该属于分组捕获的,俺觉得放这儿更好),同占有优先功能一样,它也是优先匹配,并且不交还。所以再把上面的$pattern改为这个样子,也是匹配失败滴。
$pattern = '/(?>\w+)(?=\d)/'; // 固化分组
6. 模式修饰符
模式修饰符(pattern modifier),放在模式的最后面,结束定界符之后,如'/....../imx',i、m、x表示不同的修饰符,可以一次使用多个。模式修饰符是对正则表达式整体效果起一个调节作用。
i:匹配时不区分大小写,如'/abc/i',对匹配到的abc任一个大小写都可;
m:将字符串视为多个逻辑行,还记得前面说匹配一行字符串的开头^和结尾$吗?默认preg将字符串视为一行,哪怕里边有有效的换行符,如果加了修饰符m,将带匹配字符串按照换行符分行,这些行称为逻辑行,^匹配每一个逻辑行的开头,$匹配每一个逻辑行的结尾;
s:还记得前面说的匹配任一字符的元字符 . (点号)吗?preg默认情况下点号匹配任一个字符,除了换行符,那么加了s修饰符,点号就连换行符也不拒绝了;
x:这个修饰符对于复杂的正则表达式非常有用,因为它可以在正则中添加注释!!!而且它会忽略正则中的空白符,当然,也包括换行,为了说明乱举一例:
// 使用x修饰符 $pattern = '/(\w+ \d) (?: # 这里匹配啥啥啥 "\w" | "\d" | # 这里应当这么理解那啥 [-A-Z]+ # 这里你自己猜 )/x'; $subject = 'wcwieu2832z28'; preg_match($pattern, $subject, $match);
e:它在特定的地方有用,即preg_replace方法中,preg的preg_replace方法原型如下
mixed preg_replace(mixed $pattern, mixed $replacement, mixed $subject [, int $limit = -1 [,int &$count ]])
第二个参数$replacement,会将匹配到文本替换为它,使用e修饰符,$replacement不仅可以用简单的文本,不仅可以对捕获到的文本进行$1式的引用,更流弊的是,可以写php代码,以字符串的形式(有点eval的感觉),例
$ret = preg_replace('/\d([A-Z]+)/e', 'strtolower("$1")', '5BBC'); echo 'ret=><pre>'; var_dump($ret);
可是当俺准备看结果时,报错:The /e modifier is deprecated, use preg_replace_callback instead,纳尼!模式符e被弃用了,让用preg_replace_callback这个方法,它使用一个会调函数(异步调用)来处理匹配到的结果,显然比把代码直接堆在字符串里好多了,具体怎么用可以参考手册。
D:设定了此修饰符的话,模式中的$只会匹配字符串的结尾(EOS,End Of String),而不是EOS前面的换行符,即不会看做多行。但如果设定了m修饰符将忽略此选项;
U:反转了 * 跟 *? 的含义,原来的匹配优先变为忽略优先,原来的忽略优先变为匹配优先。除了让人更加晕菜以外,好像没个鸟用。
以上,还有些强大的东西,如正则表达式递归,天,算了吧。
忘了再来看一眼 >>>逃
- 1楼forwardslash
- 小懒猫分享了好多东东,我都拜读了