前端学PHP之正则表达式基础语法

Posted 赵哲丽

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端学PHP之正则表达式基础语法相关的知识,希望对你有一定的参考价值。

前面的话

  正则表达式是用于描述字符排列和匹配模式的一种语法规则。它主要用于字符串的模式分割、匹配、查找及替换操作。在php中,正则表达式一般是由正规字符和一些特殊字符(类似于通配符)联合构成的一个文本模式的程序性描述。正则表达式有三个作用:1、匹配,也常常用于从字符串中析取信息;2、用新文本代替匹配文本;3、将一个字符串拆分为一组更小的信息块。本文将详细介绍PHP中的正则表达式基础语法

  [注意]关于javascript的正则表达式的详细信息移步至此

 

历史

  在PHP中有两套正则表达式函数库,两者功能相似,只是执行效率略有差异:一套是由PCRE(Perl Compatible Regular Expression)库提供的,使用“preg_”为前缀命名的函数;另一套由POSIX(Portable Operating System Interface of Unix)扩展提供的,使用以“ereg_”为前缀命名的函数

  PCRE来源于Perl语言,而Perl是对字符串操作功能最强大的语言之一,PHP的最初版本就是由Perl开发的产品。PCRE语法支持更多特性,比POSIX语法更强大

  在PHP4之前,主要使用POSIX;而现在,则使用主流的PCRE

  正则表达式作为一个匹配的模式,是由原子(普通字符,例如字符a到z)、特殊字符(元字符,例如*、+和?等)、以及模式修正符三部分组成的文字模式

 

定界符

  定界符常使用反斜线“/”,如“/apple/”。用户只要把需要匹配的模式内容放入定界符之间即可。作为定界的字符也不仅仅局限于“/”。除了字母、数字和斜线“\\”以外的任何字符都可以作为定界符,像“#”、“|”、“!”等都可以的

/<\\/\\w+>/              --使用反斜线作为定界符合法
|(\\d{3})-\\d+|Sm        --使用竖线”|”作为定界符合法
!^(?i)php[34]!         --使用竖线”!”作为定界符合法
/href=‘(.*)’           --非法定界符,缺少结束定界符
1-\\d3-\\d3-\\d4|         --非法定界符,缺少起始定界符

  如果分隔符需要在模式内进行匹配,它必须使用反斜线进行转义。如果分隔符经常在模式内出现, 一个更好的选择就是是用其他分隔符来提高可读性

/http:\\/\\//
#http://#

 

元字符

  正则表达式的威力源于它可以在模式中拥有选择和重复的能力。 一些字符被赋予特殊的涵义,使其不再单纯的代表自己,模式中的这种有特殊涵义的编码字符称为元字符

  共有两种不同的元字符:一种是可以在模式中方括号外任何地方使用的,另外一种是需要在方括号内使用的

  【1】在方括号外使用的元字符如下:

复制代码
\\ 一般用于转义字符
^ 断言目标的开始位置(或在多行模式下是行首)
$ 断言目标的结束位置(或在多行模式下是行尾)
. 匹配除换行符外的任何字符(默认)
[ 开始字符类定义
] 结束字符类定义
| 开始一个可选分支
( 子组的开始标记
) 子组的结束标记
? 作为量词,表示 0 次或 1 次匹配。位于量词后面用于改变量词的贪婪特性
* 量词,0 次或多次匹配
+ 量词,1 次或多次匹配
{ 自定义量词开始标记
} 自定义量词结束标记
复制代码

  [注意]在字符类外部,模式中的句点.匹配目标字符串中的任意字符,包括非打印字符, 但是默认不包括换行符。如果PCRE_DOTALL被设置,句点就会匹配换行符

  【2】模式中方括号内的部分称为“字符类”。 在一个字符类中仅有以下可用元字符:

\\ 转义字符
^ 仅在作为第一个字符(方括号内)时,表明字符类取反
- 标记字符范围

 

反斜线

  反斜线有多种用法。首先,如果紧接着是一个非字母数字字符,表明取消该字符所代表的特殊涵义。这种将反斜线作为转义字符的用法在字符类内部和外部都可用

  比如,如果希望匹配一个 "*" 字符,需要在模式中写为"\\*"。这适用于一个字符在不进行转义会有特殊含义的情况下。但是,对于非数字字母的字符,总是在需要其进行原文匹配的时候在它前面增加一个反斜线,来声明它代表自己,这是安全的。如果要匹配一个反斜线,那么在模式中使用 ”\\\\”

  反斜线在单引号字符串和双引号字符串中都有特殊含义,因此要匹配一个反斜线,模式中必须写为 ”\\\\\\\\” 

  [注意] “/\\\\/”, 首先它作为字符串,反斜线会进行转义, 那么转义后的结果是/\\/,这个才是正则表达式引擎拿到的模式,而正则表达式引擎也认为\\是转义标记,它会将分隔符/进行转义,从而得到的是一个错误,因此,需要4个反斜线才可以匹配一个反斜线

  反斜线的第二种用途提供了一种对非打印字符进行可见编码的控制手段。除了二进制的0 会终结一个模式外,并不会严格的限制非打印字符(自身)的出现,但是当一个模式以文本编辑器的方式编辑准备的时候,使用下面的转义序列相比使用二进制字符会更加容易

复制代码
\\a      响铃字符(十六进制 07)
\\cx     "control-x",x 是任意字符
\\e      转义 (十六进制 1B)
\\f      换页 (十六进制 0C)
\\n      换行 (十六进制 0A)
\\p{xx}  一个符合 xx 属性的字符
\\P{xx}  一个不符合xx属性的字符
\\r      回车 (十六进制 0D)
\\t      水平制表符 (十六进制 09)
\\xhh    hh十六进制编码的字符
\\ddd    ddd八进制编码的字符,或者后向引用
复制代码

字符类

复制代码
\\d  任意十进制数字
\\D  任意非十进制数字
\\h  任意水平空白字符
\\H  任意非水平空白字符
\\s  任意空白字符
\\S  任意非空白字符
\\v  任意垂直空白字符
\\V  任意非垂直空白字符
\\w  任意单词字符
\\W  任意非单词字符
复制代码

  [注意]单词字符指的是任意字母、数字、下划线。也就是说任意可以组成perl单词的字符

  反斜线的第四种用法是一些简单的断言。一个断言指定一个必须在特定位置匹配的条件, 它们不会从目标字符串中消耗任何字符。反斜线断言包括:

复制代码
\\b  单词边界
\\B  非单词边界
\\A  目标的开始位置(独立于多行模式)
\\Z  目标的结束位置或结束处的换行符(独立于多行模式)
\\z  目标的结束位置(独立于多行模式)
\\G  在目标中首次匹配位置
复制代码

  [注意]\\A、\\Z、\\z断言不同于传统的^和$,因为他们永远匹配目标字符串的开始和结尾,而不会受模式修饰符的限制

 

  在一个字符类外面,在默认匹配模式下,^是一个断言当前匹配点位于目标字符串开始处的断言。在一个字符类内部,^表明这个字符类中描述的字符取反

  ^并不一定要是模式的第一个字符,但是如果处于某个可选分支时,它应该是该分支的首字符。如果所有选择分支都以 ^ 开头,这就是说,如果模式限制为只匹配目标的开头, 它被称为是一个 ”紧固” 模式

  美元符号是用于断言当前匹配点位于目标字符串末尾,或当目标字符串以换行符结尾时当前匹配点位于该换行符位置(默认情况)。$不一定要作为模式的最后一个字符,但是如果它在某个可选分支中时,就应该位于该分支的末尾。美元符号在字符类中没有特殊的意义

  美元符号的意义可以通过在编译或匹配时设置PCRE_DOLLAR_ENDONLY 改变为只匹配字符串末尾。 这不会影响\\Z断言的行为

  ^ 和美元符号字符的意义在 PCRE_MULTILINE 选项被设置时会发生变化。当在这种情况下时, 它们匹配每一个换行符后面的和前面的字符,另外,也会匹配目标字符串的开始和结束。比如,模式 /^abc美元符号/ 在多行模式下会成功匹配目标字符串 ”def\\nabc”,而正常情况下不会。因此,由于所有的可选分支都以 ^ 开始,在单行模式下这成为紧固模式,然而在多行模式下,这是非紧固的。PCRE_DOLLAR_ENDONLY选项在PCRE_MULTILINE 设置后失效

美元符号  $

 

字符类

  左方括号开始一个字符类的描述,并以方中括号结束。单独的一个右方括号没有特殊含义。如果一个右方括号需要作为一个字符类中的成员,那么可以将它写在字符类的首字符处(如果使用了^取反,那么是第二个)或者使用转义符

  一个字符类在目标字符串中匹配一个单独的字符;该字符必须是字符类中定义的字符集合的一个,除非使用^对字符类取反。如果^需要作为一个字符类的成员,确保它不是该字符类的首字符,或者对其进行转义即可

  例如,字符类[aeiou]匹配所有的小写元音字母,而[^aeiou]匹配所有非元音字母的字符。注意:^只是一个通过枚举指定那些不存在字符类之中的字符的便利符号。而不是断言,它仍然会从目标字符串中消耗一个字符,并且如果当前匹配点在目标字符串末尾,匹配将会失败

  当大小写无关匹配被设置后,任意字符类都同时代表大小写两种版本,因此对于例子, 一个大小写不敏感的[aeiou]同时匹配"A"和"a",并且大小写不敏感的[^aeiou]同时不匹配"A"

  换行符在字符类中没有任何特殊涵义,与 PCRE_DOTALL 或 PCRE_MULTILINE 选项都无关。 一个字符类比如 [^a] 始终会匹配换行符。

  在字符类中,一个中划线(减号 -)可以用于指定从一个字符到另一个字符的范围。 比如,[d-m]匹配d到m之间的所有字符,这个集合是闭合的。如果中划线自身要在一个字符类中描述,它必须被转移或者出现在一个不会被解释为一个范围的位置,典型的比如字符类开始或结束位置

  在一个字符范围描述后面不能使用右中括号。 比如一个模式 [W-]46]被解释为一个包含 W 和 - 的字符类,后面紧跟字符串 ”46]”,因此它可以匹配 ”W46]” 或 ”-46]”。然而,如果中括号是经过转义的,它将会被解释为范围的终点,因此 [W-\\]46]就会被解释为一个单独的包含 W 至 ] 范围内所有字符以及 4、6 的字符类。 8进制或16进制描述的中括号同样可以用于作为范围的终点。

  范围操作以ASCII整理排序。它们可以用于为字符指定数值,比如 [\\000-\\037]。 如果在大小写不敏感匹配模式下使用一个包含字母的范围,则同时匹配它的大小写形式。 比如 [W-c] 在不区分大小写匹配时等价于 [][\\^_`wxyzabc],并且,如果使用了 ”fr”(法国) 的地域设置字符表时,[\\xc8-xcb] 将会在所有模式下匹配重音E字符

  字符类\\d、\\D、 \\s、\\S、\\w 和 \\W 也可以出现在一个字符类中,用以将其匹配的字符类加入到新的自定义字符类中。比如,[\\dABCDEF]匹配任意合法的16进制数。用^可以很方便的制定严格的字符类,比如[^\\W_]匹配任何字母或数字,但不匹配下划线。

  所有非字母数字字符除了\\、-、 ^(在起始位置)以及结束的]在字符类中都是非特殊字符,没有转义也不会有危害。模式结束符在表达式中总是特殊字符,必须进行转义

 

可选路径

  竖线字符(|)用于分离模式中的可选路径。 比如模式gilbert|Sullivan匹配 ”gilbert” 或者 ”sullivan”。 竖线可以在模式中出现任意多个,并且允许有空的可选路径(匹配空字符串)。 匹配的处理从左到右尝试每一个可选路径,并且使用第一个成功匹配的。 如果可选路径在子组中,则”成功匹配”表示同时匹配了子模式中的分支以及主模式中的其他部分

 

模式修正符

  模式修正符在正则表达式定界符之外使用,一般地在最后一个斜线之后。模式修正符可以调整正则表达式的解释,扩展了正则表达式在匹配、替换等操作时的某些功能,而且模式修正符也可以组合使用,更增强了正则表达式的处理能力

  模式修正符对编写简洁而短小的表达式大有帮助,下面列出了一些常用的模式修正符及其功能说明

i  在模式进行匹配时不区分大小写

m  将字符串视为多行。默认的正则开始^和结束$将目标字符串作为单一的一行字符。如果使用m修饰符,那么开始和结束将会指向字符串的每一行

s  模式中的点字符.匹配所有的字符,包括换行符

x  模式中的空白忽略不计,除非它已经被转义

e  只用在preg_replace()函数中,在替换字符串中对逆向引用做正常的替换,将其作为PHP代码求值,并用其结果来替换所搜索的字符串

U  本修正符反转了匹配数量的值使其不是默认的重复,而变成在后面跟上?才变得重复,这和Perl不兼容。也可以通过在模式之中设定U修正符或者在数量符之后跟一个问号?来开启此选项

D  模式中的美元符号仅匹配目标字符串的结尾。没有些选项时,如果最后一个字符是换行符,美元符号也会匹配此字符之前。如果设定了m修正符则忽略此选项 

子组

  子组(子模式)通过圆括号分隔界定,并且它们可以嵌套。将一个模式中的一部分标记为子组(子模式)主要是来做两件事情:

  1、将可选分支局部化。比如,模式cat(arcat|erpillar|)匹配 ”cat”、“cataract”、“caterpillar” 中的一个,如果没有圆括号的话,它匹配的则是 ”cataract”、“erpillar” 以及空字符串

  2、将子组设定为捕获子组。当整个模式匹配后,目标字符串中匹配子组的部分将会通过 pcre_exec()()的ovector参数回传给调用者。左括号从左至右出现的次序就是对应子组的下标(从 1 开始), 可以通过这些下标数字来获取捕获子模式匹配结果。

  比如,如果字符串 "the red king" 使用模式((red|white) (king|queen)) 进行匹配, 模式匹配到的结果是 array("red king", "red king", "red", "king") 的形式, 其中第 0 个元素是整个模式匹配的结果,后面的三个元素依次为三个子组匹配的结果。 它们的下标分别为 1、2、3

  事实上,圆括号履行的两种功能并不总是有用的。经常我们会有一种需求需要使用子组进行分组, 但又不需要(单独的)捕获它们。 在子组定义的左括号后面紧跟字符串 "?:" 会使得该子组不被单独捕获,并且不会对其后子组序号的计算产生影响。比如,如果字符串 "the white queen" 匹配模式 ((?:red|white) (king|queen)),匹配到的结果会是 array("white queen"、"white queen"、"white queen")和 king|queen 这两个子组。 捕获子组序号的最大值是99,最大允许拥有的所有子组(包括捕获的和非捕获的)的最大数量为 200。

  为了方便简写,如果需要在非捕获子组开始位置设置选项, 选项字母可以位于 ? 和 : 之间,比如:

(?i:saturday|sunday)
(?:(?i)saturday|sunday)

  上面两种写法实际上是相同的模式。因为可选分支会从左到右尝试每个分支, 并且选项没有在子模式结束前被重置,并且由于选项的设置会穿透对后面的其他分支产生影响,因此,上面的模式都会匹配 "SUNDAY" 以及 "Saturday"

  在 PHP 4.3.3 中,可以对子组使用 (?P<name>pattern)的语法进行命名。这个子模式将会在匹配结果中同时以其名称和顺序(数字下标)出现, PHP 5.2.2中又增加了两种味子组命名的语法:(?<name>pattern) 和 (?’name’pattern)

  有时需要多个匹配可以在一个正则表达式中选用子组。 为了让多个子组可以共用一个后向引用数字的问题, (?| 语法允许复制数字

  考虑下面的正则表达式匹配Sunday:

(?:(Sat)ur|(Sun))day

  这里当后向引用 1 空时Sun 存储在后向引用 2 中。当后向引用 2 不存在的时候 Sat 存储在后向引用 1中。 使用 (?|修改模式来修复这个问题:

(?|(Sat)ur|(Sun))day

  使用这个模式, Sun和Sat都会被存储到后向引用1中

 

量词

  重复次数是通过量词指定的,可以紧跟在下面元素之后:单独的字符,可以是经过转义的;元字符;字符类;后向引用;子组(除非它是一个断言)

  一般的重复量词指定了一个最小数值和一个最大数值的匹配次数,通过花括号包裹两个数字,两个数字之间用逗号隔开的语法定义。两个数值都必须小于65536,并且第一个数字必须小于等于第二个

  比如:z{2,4}匹配 ”zz”,“zzz”,“zzzz”。单个的右花括号不是特殊字符。如果第二个数字被省略,但是逗号仍然存在,就代表没有上限;如果第二个数字和逗号都被省略,那么这个量词就限定的是一个确定次数的匹配。比如:[aeiou]{3,}匹配至少三个连续的元音字母,但是同时也可以匹配更多,而\\d{8} 则只能匹配8个数字。左花括号出现在不允许使用量词的位置或者与量词语法不匹配时,被认为是一个普通字符,对它自身进行原文匹配。比如,{,6}就不是一个量词,会按照原文匹配四个字符 ”{,6}”

  量词{0}是被授权的,它会导致的行为是认为前面的项和量词不存在

  为了方便(以及历史的兼容性),最常用的三个量词都有单字符缩写

*    等价于 {0,}
+    等价于 {1,}
?    等价于 {0,1}

  可以通过一个不匹配任何字符的子模式后面紧跟一个匹配0或多个字符的量词来构造一个没有上限的无限循环。比如:(a?)*

  默认情况下,量词都是“贪婪”的,也就是说,它们会在不导致模式匹配失败的前提下,尽可能多的匹配字符(直到最大允许的匹配次数)。 这种问题的典型示例就是尝试匹配C语言的注释。出现在/*和*/之间的所有内容都被认为是注释。在注释中间,可以允许出现单独的 * 和 /

  对C注释匹配的一个尝试是使用模式 /\\*.*\\*/, 假设将此模式应用在字符串 ”/* first comment*/ not comment /*second comment*/” 它会匹配到错误的结果,也就是整个字符串, 这是因为量词的贪婪性导致的,它会尝试尽可能多的匹配字符。

  然而,如果一个量词紧跟着一个 ?(问号) 标记,它就会成为懒惰(非贪婪)模式, 它不再尽可能多的匹配,而是尽可能少的匹配。 因此模式 /\\*.*?\\*/ 在C的注释匹配上将会正确的执行。各个量词自身的意义并不会改变,而是由于加入了?使其首选的匹配次数发生改变。不要将?的这个用法和它作为量词的用法混淆。因为它又两种用法,因此有时它会出现量词,比如\\d??\\d会更倾向于匹配一个数字,但同时如果为了达到整个模式匹配的目的,它也可以接受两个数字的匹配。译注: 以模式 \\w\\d??\\d\\w为例,对于字符串”a33a”,虽然\\d??是非贪婪的,但由于如果使用贪婪会导致整个模式不匹配。所以,最终它选择的仍然是匹配到一个数字

  如果 PCRE_UNGREEDY 选项被设置(一个在 perl 中不可用的选项),那么量词默认情况下就是非贪婪的了。但是,单个的量词可以通过紧跟一个?来使其成为贪婪的。换句话说,PCRE_UNGREEDY这个选项逆转了贪婪的默认行为

  量词后面紧跟一个“+”是“占有”性。它会吃掉尽可能多的字符,并且不关注后面的其他模式,比如,.*abc 匹配 ”aabc”,但是 .*+abc 不会匹配,因为.*+会吃掉整个字符串,从而导致后面剩余的模式得不到匹配。自PHP 4.3.3 起,可以使用占有符 (+) 修饰量词来达到提升速度的目的

  当一个子组受最小数量大于1或有一个最大数量限制的量词修饰时,按照最小或最大的数量的比例需要更多的存储用于编译模式

  如果一个模式以 .* 或 .{0,} 开始并且PCRE_DOTALL选项开启(等价于 perl 的/s),也就是允许.匹配换行符,那么模式会隐式的紧固,因为不管怎么样,接下来都会对目标字符串中的每个字符位置进行尝试,因此在第一次之后,在任何位置都不会有一个对所有匹配重试的点。PCRE会想对待\\A一样处理这个模式。 在我们已知目标字符串没有包含换行符的情况下,当模式以.*开始的时候我们为了获得这个优化,值得设置 PCRE_DOTALL,或者选择使用 ^ 明确指明锚定

  当一个捕获子组时重复的时,捕获到的该子组的结果是最后一次迭代捕获的值。比如,(tweedle[dume]{3}\\s*)+匹配字符串“tweedledum tweedledee”,得到的的子组捕获结果是”tweedledee”。然而,如果是嵌套的捕获子组,相应的捕获值可能会被设置到之前的迭代中。比如,/(a|(b))+/ 匹配字符串 ”aba”, 第二个捕获子组得到的结果会是 ”b”

 

后向引用

  在一个字符类外面,反斜线紧跟一个大于0(可能还有一位数)的数字就是一个到模式中之前出现的某个捕获组的后向引用

  如果紧跟反斜线的数字小于10,它总是一个后向引用,并且如果在模式中没有这么多的捕获组会引发一个错误。换一种说法,被引用的括号不能少于被引用的小于10的数量

  一个后向引用会直接匹配被引用捕获组在目标字符串中实际捕获到的内容,而不是匹配子组模式的内容。因此,模式(sens|respons)e and \\1ibility将会匹配 ”sense and sensibility” 和 ”response and responsibility”, 而不会匹配 ”sense and responsibility”

  如果在后向引用时被强制进行了大小写敏感匹配, 比如 ((?i)rah)\\s+\\1 匹配 ”rah rah”和”RAH RAH”,但是不会匹配 ”RAH rah”, 即使原始捕获子组自身是不区分大小写的

  可能会有超过一个的后向引用引用相同的子组。一个子组可能并不会真正的用于特定的匹配,此时,任何对这个子组的后向引用也都会失败。 比如,模式 (a|(bc))\\2 总是在匹配 ”a” 开头而不是 ”bc” 开头的字符串时失败。因为可能会有多达99个后向引用,所有紧跟反斜线后的数字都可能是一个潜在的后向引用计数。如果模式在后向引用之后紧接着还是一个数值字符,那么必须使用一些分隔符用于终结后向引用语法。如果PCRE_EXTENDED选项被设置了,可以使用空格来做。其他情况下可以使用一个空的注释

  如果一个后向引用出现在它所引用的子组内部,它的匹配就会失败。比如,(a\\1)就不会得到任何匹配。然而这种引用可以用于内部的子模式重复。比如,模式(a|b\\1)+会匹配任意数量的“a”组成的字符串以及“aba”,“ababba”等等。在每次子模式的迭代过程中, 后向引用匹配上一次迭代时这个子组匹配到的字符串。为了做这种工作,模式必须满足这样一个条件,模式在第一次迭代的时候,必须能够保证不需要匹配后向引用。这种条件可以像上面的例子用可选路径来实现,也可以通过使用最小值为 0 的量词修饰后向引用的方式来完成

  在 PHP 5.2.2之后,\\g 转义序列可以用于子模式的绝对和相对引用。这个转义序列必须紧跟一个无符号数字或一个负数,可以选择性的使用括号对数字进行包裹。序列\\1,\\g1,\\g{1}之间是同义词关系。这种用法可以消除使用反斜线紧跟数值描述反向引用时候产生的歧义。这种转义序列有利于区分后向引用和八进制数字字符,也使得后向引用后面紧跟一个原文匹配数字变的更明了,比如 \\g{2}1

  \\g 转义序列紧跟一个负数代表一个相对的后向引用。比如:(foo)(bar)\\g{-1}可以匹配字符串”foobarbar”, (foo)(bar)\\g{2} 可以匹配 ”foobarfoo”。 这在长的模式中作为一个可选方案, 用来保持对之前一个特定子组的引用的子组序号的追踪

  后向引用也支持使用子组名称的语法方式描述, 比如 (?P=name) 或者 PHP 5.2.2 开始可以实用\\k<name> 或 \\k’name’。 另外在 PHP 5.2.4 中加入了对\\k{name} 和 \\g{name} 的支持

 

断言

  一个断言就是一个对当前匹配位置之前或之后的字符的测试,它不会实际消耗任何字符。简单的断言代码有\\b、\\B、 \\A、 \\Z、\\z、 ^、$ 等等。 更加复杂的断言以子组的方式编码。 它有两种类型:前瞻断言(从当前位置向前测试)和后瞻断言(从当前位置向后测试)

  一个断言子组的匹配还是通过普通方式进行的,不同在于它不会导致当前的匹配点发生改变。前瞻断言中的正面断言(断言此匹配为真)以“(?=”开始,消极断言以“(?!”开头。比如,\\w+(?=;)匹配一个单词紧跟着一个分号但是匹配结果不会包含分号,foo(?!bar)匹配所有后面没有紧跟“bar”的“foo”字符串。注意一个类似的模式(?!foo)bar,它不能用于查找之前出现所有不是“foo”的“bar”匹配,它会查找到任意的“bar”出现的情况,因为 (?!foo)这个断言在接下来三个字符时“bar”的时候是永远都TRUE的。前瞻断言需要达到的就是这样的效果

  后瞻断言中的正面断言以“(?<=”开始, 消极断言以“(?<!”开始。比如,(?<!foo)bar用于查找任何前面不是“foo”的“bar”。后瞻断言的内容被严格限制为只能用于匹配定长字符串。但是,如果有多个可选分支,它们不需要拥有相同的长度。比如 (?<=bullock|donkey)是允许的, 但是 (?<!dogs?|cats?)将会引发一个编译期的错误。在最上级分支可以匹配不同长度的字符串是允许的。相比较于perl5.005而言,它会要求多个分支使用相同长度的字符串匹配。(?<=ab(c|de))这样的断言是不允许的,因为它单个的顶级分支可以匹配两个不同的长度,但是它可以接受使用两个顶级分支的写法 (?<=abc|abde) 这样的断言实现,对于每个可选分支,暂时将当前位置移动到尝试匹配的当前位置之前的固定宽度处。 如果在当前没有足够的字符就视为匹配失败。后瞻断言与一次性子组结合使用可以用来匹配字符串结尾; 一个例子就是在一次性子组上给出字符串结尾

  多个断言(任意顺序)可以同时出现。 比如 (?<=\\d{3})(?< !999)foo 匹配前面有三个数字但不是“999”的字符串“foo”。注意,每个断言独立应用到对目标字符串该点的匹配。首先它会检查前面的三位都是数字,然后检查这三位不是“999”。这个模式不能匹配“foo”前面有三位数字然后紧跟3位非999共6个字符的字符串,比如,它不匹配“123abcfoo”。匹配“123abcfoo” 这个字符串的模式可以是(?<=\\d{3}…)(?< !999)foo。这种情况下,第一个断言查看(当前匹配点)前面的6个字符,检查前三个是数字,然后第二个断言检查(当前匹配点)前三个字符不是“999”

  断言可以以任意复杂度嵌套。 比如 (?<=(?<!foo)bar)baz 匹配前面有“bar”但是“bar”前面没有“foo” 的“baz”。另外一个模式 (?<=\\d{3}…(?< !999))foo则匹配前面有三个数字字符紧跟3个不是999的任意字符的 ”foo”

  断言子组时非捕获子组,并且不能用量词修饰,因为对同一件事做多次断言是没有意义的。如果所有的断言都包含一个捕获子组,那么为了在整个模式中捕获子组计数的目的,它们都会被计算在内。然而, 子字符串的捕获仅可以用于正面断言,因为对于消极的断言是没有意义的。

  将断言计算在内,可以拥有的最大子组数量是 200 个

 

一次性子组

  对于同时有最大值和最小值量词限制的重复项,在匹配失败后,紧接着会以另外一个重复次数重新评估是否能使模式匹配。当模式的作者明确知道执行上没有问题时,通过改变匹配的行为或者使其更早的匹配失败以阻止这种行为是很有用的

  考虑一个例子,模式\\d+foo应用到目标行123456bar时:

  在匹配了6个数字后匹配”foo”时失败,通常的行为时匹配器尝试使\\d+只匹配5个数字,只匹配4个数字,在最终失败之前依次进行尝试。一次性子组提供了一种特殊的意义,当模式的一部分得到匹配后,不再对其进行重新评估,因此匹配器在第一次匹配“foo”失败后就能立刻失败。语法符号是另外一种特殊的括号,以(?>开始,比如(?>\\d+)bar

  这种括号对模式的一部分提供了”锁定”,当它包含一个匹配之后,会阻止未来模式失败后对它内部的后向回溯。后向回溯在这里失效,其他工作照常进行

  换一种说法,如果在目标字符串中当前匹配点是锚点,这种类型的子组匹配的字符串等同于一个独立的模式匹配。

  一次性子组不是捕获子组。如上面的例子,简单而言,就是尽其所能吃掉尽可能多的匹配字符。因此,尽管\\d+和\\d+?都会调整要匹配的数字的个数以便模式的其他部分匹配,(?>\\d+)却仅能匹配整个数字序列

  这个(语法)结构可以包含任意复杂度的字符,也可以嵌套

  一次性子组可以和后瞻断言结合使用来指定在目标字符串末尾的有效匹配。考虑当一个简单的模式比如abcd应用到一个不匹配的长字符串上。由于匹配时从左到右处理的,PCRE会从目标中查找每一个”a”然后查看是否紧接着会匹配模式的剩余部分。如果模式是.∗abcd应用到一个不匹配的长字符串上。由于匹配时从左到右处理的,PCRE会从目标中查找每一个”a”然后查看是否紧接着会匹配模式的剩余部分。如果模式是.∗abcd,那么初始的.*将首先匹配整个字符串,但是当它失败后(因为紧接着不是”a”),它会回溯所有的匹配,依次吐出最后1个字符,倒数第2个字符等等。从右向左查找整个字符串中的”a”,因此,我们不能很好的退出。然而,如果模式写作^(?>.*)(?<=abcd)那么它就不会回溯.*这一部分,它仅仅用于匹配整个字符串。后瞻断言对字符串末尾的后四个字符做了一个测试。如果它失败,匹配立即失败。对于长字符串,这个模式将会带来显著的处理时间上的性能提升。

  当一个模式中包含一个子组自己可以无限重复并且内部有无限重复元素时,使用一次性子组是避免一些失败匹配消耗大量时间的唯一途径。模式(\\D+|<\\d+>)*[!?]匹配一个不限制数目的非数字字符或由<>闭合的数字字符紧跟着!或?。当它匹配的时候,运行时快速的。然而,如果它应用到”aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa”上将会在报告错误之前消耗很多时间。这是因为字符串可以用于两种重复规则,并且需要为两种重复规则都分配进行尝试。(示例的结尾使用[!?]而不是单个的字符,是因为PCRE和perl都会对模式最后是一个单独字符时的快速报错有优化。它们会记录最后需要匹配的单个字符,当它们没有出现在字符串中时快速报错。)如果模式修改为((?>\\D+)|<\\d+>)*[!?]就会快速得到报错

 

条件子组

  可以使匹配器根据一个断言的结果,或者之前的一个捕获子组是否匹配来条件式的匹配一个子组或者在两个可选子组中选择。条件子组的两种语法如下:

(?(condition)yes-pattern)
(?(condition)yes-pattern|no-pattern)

  如果条件满足,使用yes-pattern,其他情况使用no-pattern(如果指定了)。如果有超过2个的可选子组,会产生给一个编译期错误

  条件一共有两种。如果在(condition)的括号内是数字组成的文本,条件在该数字代表的(之前的)子组得到匹配时满足(即使用yes-pattern)。考虑下面的模式,为了使其易于阅读其中增加了一些空白字符(查看PCRE_EXTENDED选项)并且将其分为三个部分:()?[()]+(?(1))?[()]+(?(1))

  模式的第一部分匹配一个可选的左括号,并且如果该字符出现,设置其为第一个子组的捕获子串。第二部分匹配一个或多个非括号字符。第三部分是一个条件子组,它会测试第一个子组是否匹配,如果匹配到了,也就是说目标字符串以左括号开始,条件为TRUE,那么使用yes-pattern也就是这里需要匹配一个右括号。其他情况下,既然no-pattern没有出现,这个子组就不匹配任何东西。换句话说,这个模式匹配一个没有括号的或者闭合括号包裹的字符序列。

  如果条件式字符串(R),它在得到对模式或子模式的递归调用时满足。在”最上级”,条件总是false。

  如果条件不是数字序列或(R),它就必须是一个断言。这里的断言可以使任意的,积极,消极,正向,后向都是可以的。考虑这个模式,同样为了方便阅读, 增加了一些空白字符,并且在第二行有两个可选路径

(?(?=[^a-z]*[a-z])
\\d{2}-[a-z]{3}-\\d{2} | \\d{2}-\\d{2}-\\d{2} )

  条件式一个正向积极断言,匹配一个可选的非小写字母字符序列紧接着一个小写字母。 换一种说法,它测试目标中至少出现一个小写字母,如果小写字母发现, 目标匹配第一个可选分支,其他情况下它匹配第二个分支。 这个模式匹配两种格式的字符串:dd-aaa-dd 或 dd-dd-dd。aaa 代表小写字母, dd 是数字

 

注释

  字符序列(?#标记开始一个注释直到遇到一个右括号。不允许嵌套括号。注释中的字符不会作为模式的一部分参与匹配

  如果设置了 PCRE_EXTENDED 选项, 一个字符类外部的未转义的 # 字符就代表本行剩余部分为注释

 

递归模式

  考虑匹配圆括号内字符串的问题,允许无限嵌套括号。如果不使用递归,最好的方式是使用一个模式匹配固定深度的嵌套。它不能处理任意深度的嵌套。perl5.6提供了一个实验性的功能允许正则表达式递归。特殊项(?R)提供了递归的这种特殊用法。这个PCRE模式解决了圆括号问题(假设PCRE_EXTENDED选项被设置了,因此空白字符被忽略):((?>[()]+)|(?R))∗((?>[()]+)|(?R))∗。

  首先,它匹配一个左括号。然后它匹配任意数量的非括号字符序列或一个模式自身的递归匹配(比如,一个正确的括号子串),最终,匹配一个右括号。

  这个例子模式包含无限重复的嵌套,因此使用了一次性子组匹配非括号字符,这在模式应用到模式不匹配的字符串时非常重要。比如,当它应用到(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa()时就会很快的产生”不匹配”结果。然而,如果不使用一次性子组,这个匹配将会运行很长时间,因为有很多途径让+和*重复限定分隔目标字符串,并且在报告失败之前需要测试所有路径。

  所有捕获子组最终被设置的捕获值都是从递归最外层子模式捕获的值。如果上面的模式匹配(ab(cd)ef),捕获子组最终被设置的值为”ef”,即顶级得到的最后一个值。如果增加了额外的括号,(((?>[()]+)|(?R))∗)(((?>[()]+)|(?R))∗),捕获到的字符串就是顶层括号的匹配内容”ab(cd)ef”。如果在模式中有超过15个捕获括号,PCRE在递归期间就会使用pcre_malloc分配额外的内存来存储数据,随后通过pcre_free释放他们。如果没有内存可被分配,它就仅保存前15个捕获括号,在递归内部无法给出内存不够用的错误。

  从PHP4.3.3开始,(?1)、(?2)等可以用于递归子组。这同样可以用于命名子组:(?P>name)或(?P&name)。

  如果递归子组语法在它提到的子组括号外部使用(无论是子组数字序号还是子组名称),这个操作就相当于程序设计语言中的子程序。前面一些有一个例子指出模式(sens|respons)eand\\1ibility匹配 ”sense and responsibility” 和 ”response and responsibility”,但是不匹配 ”sense and responsibility”。如果用模式 (sens|respons)e and (?1)ibility 替代, 它会像匹配那两个字符串一样匹配 ”sense and responsibility”。 这种引用方式意义是紧接着匹配引用的子模式

  目标字符串的最大长度是 int 型变量可以存储的最大正整数。然而, PCRE 使用递归处理子组和无限重复。 这就是说对于某些模式可用的栈空间可能会受目标字符串限制

 

性能

  模式中一些项可能比其他一些更加高效。比如使用[aeiou]这样的字符类会比可选路径(a|e|i|o|u)高效。一般而言,用尽可能简单的构造描述需求是最高效的

  当一个模式以.*开始并且设置了PCRE_DOTALL选项时,模式通过PCRE隐式锚定,因为它可以匹配字符串的开始。然而,如果PCRE_DOTALL没有设置,PCRE不能做这个优化,因为.元字符不能匹配换行符,如果目标字符串包含换行符,模式可能会从一个换行符后面开始匹配,而不是最开始位置。比如,模式(.*)second匹配目标字符串”first\\nandsecond”(\\n是一个换行符)第一个捕获子组结果是”and”。为了这样做,PCRE尝试从目标字符串中每个换行符后开始匹配

  如果使用模式匹配没有换行符的目标字符串,可以通过设置PCRE_DOTALL或以^.*开始的模式明确指示锚定以获取最佳性能。这样节省了PCRE沿目标字符串扫描查找换行符重新开始的时间。

  小心模式中的无限重复嵌套。这在应用到不匹配字符串时可能会导致运行时间很长。考虑模式片段(a+)*

  对于一些简单的情况的优化是像(a+)*b这样紧接着使用原文字符串.。在着手正式匹配工作之前,PCRE检查目标字符串后面是否有”b”字符,如果没有就立即失败。然而当紧接着没有原文字符的时候这个优化是不可用的。你可以比较观察(a+)*\\d和上面模式的行为差异。前者在应用到整行的”a”组成的字符串时几乎是立即报告失败,而后者在目标字符串长于20个字符时,时间消耗就相当可观

以上是关于前端学PHP之正则表达式基础语法的主要内容,如果未能解决你的问题,请参考以下文章

前端学PHP之正则表达式函数

前端学PHP之Smarty模板引擎

Scala之option类型及偏函数使用异常处理正则表达式

图解前端之正则表达式

#yyds干货盘点# Python 内置模块之 re 库,一文搞定正则表达式初阶用法,滚雪球学 Python 第 13 篇

PHP 正则表达式总结