PHP 中的正则表达式详解

简介

正则表达式(Regular Expression),也简称为 RE、Reg、RegEx 或 RegExp。

正则表达式的作用是用来查找或者替换符合某种模式(规则)的字符串。

正则表达式是一个从左到右匹配目标字符串的模式(pattern)。大多数字符自身就代表一个匹配自身的模式。

在 PHP 中,有两种处理正则表达式的扩展库。

  • POSIX 扩展
  • PCRE 扩展

POSIX 扩展

自 PHP 5.3.0 起, POSIX 扩展已被废弃,在 PHP 7 中已经移除了 POSIX 扩展。因此,不推荐使用 POSIX 库提供的函数。

POSIX 库有专门的处理大小写不敏感的函数,正则匹配的结果和速度与 PCRE 有所区别。

POSIX 库的函数集主要是以 ereg 开头的函数,但也有例外。例如:ereg、eregi、ereg_replace、eregi_replace、split、spliti 和 sql_regcase。

上述 POSIX 库相关的函数,都不推荐使用。

PCRE 扩展

PCRE 是 Perl-compatible regular expression 的缩写,即兼容 perl 的正则表达式。

PCRE 库是 PHP 的核心扩展,总是启用的。 PHP 默认就内置了 PCRE library。

PCRE 库是一个实现了与 perl 5 略有差异的正则表达式模式匹配功能的函数集。

扫描二维码关注公众号,回复: 1645701 查看本文章

PCRE 库的函数都是以 preg_ 开头的函数,例如:preg_match、preg_match_all、preg_replace、preg_split、preg_filter 等。

推荐使用 PCRE 库的函数来处理正则表达式。

更多内容可参考:http://php.net/manual/zh/book.pcre.php

综上,PHP 中主要采用 PCRE 库来操作正则表达式。

下面,我们将详细介绍 PCRE 的正则(模式)语法。

分隔符

当使用 PCRE 函数时,模式(正则表达式)需要由分隔符闭合包裹。

分隔符可以是任意的非字母数字、非反斜线、非空白字符。

常用的分隔符有正斜线 (/)、hash符号 (#) 以及取反符号 (~) 等等。

下面的例子都是合法的模式。

/foo bar/
#^[^0-9]$#
+php+
%[a-zA-Z0-9_-]%

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

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

需要将一个字符串放入模式中使用时,可以用 preg_quote() 函数,对字符串中的正则表达式特殊字符进行转义。

正则表达式的特殊字符有: . \ + * ? [ ^ ] $ ( ) { } = ! < > | : -

也可以使用括号样式的分隔符,左括号和右括号分别作为开始和结束分隔符。

{this is a pattern}

元字符

模式中的一些字符被赋予 特殊的含义,它不再单纯的代表自身,这种有特殊含义的字符称为元字符。

元字符可以分为两类:

  • 在方括号外使用的元字符
  • 在方括号内使用的元字符

在方括号外使用的元字符

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

在方括号内使用的元字符

  • \ 一般用于转义字符
  • ^ 仅在作为方括号内的第一个字符时,表示对整个字符簇取反
  • - 字符范围标记

转义序列

转义序列(反斜线)有多种用法。

第一种用法

如果反斜线紧接着是一个非字母数字字符,表示取消该字符所代表的特殊含义。

例如, /\./ 表示匹配 . 本身。

它适用于当一个字符不进行转义时会有特殊的含义,要想匹配字符本身,就需要用反斜线取消特殊含义。

第二种用法

反斜线提供了一种对非打印字符进行可见编码的控制手段。

  • \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八进制编码的字符,或者后向引用
  • \040 空格的另外一种用法
  • \40 当提供了少于 40 个子表达式时,也认为是空格
  • \7 始终是后向引用
  • \11 可能是后向引用,也可能是制表符
  • \011 总是一个制表符( \0 后面是两个八进制数)
  • \0113 一个制表符紧跟着一个3
  • \113 八进制 113 代表的字符

注意,八进制值的 100 或者更大的值必须没有前置的 0 引导, 因为每次最多读取 3 个 8 进制位。

所有序列定义的单字节值都可以在字符簇内部或外部使用。

另外,在字符簇内, 序列 \b 解释为退格字符。在字符簇外,它又有不同的含义。

第三种用法

反斜线可以用来表示特殊的字符簇(字符集合或字符类)。

  • \d 任意数字(十进制),等价于 [0-9]
  • \D 表示对 \d 取反,即任意非数字,等价于 [^0-9]
  • \h 任意水平空白字符
  • \H 任意非水平空白字符
  • \s 任意空白字符
  • \S 任意非空白字符
  • \v 任意垂直空白字符
  • \V 任意非垂直空白字符
  • \w 任意字母、数字、下划线,等价于 [a-zA-Z0-9_]
  • \W 任意非字母、数字、下划线,等价于 [^a-zA-Z0-9_]

上面的每一个转义序列都代表了完整字符集中两个不相交的部分, 任意字符一定会匹配其中一个,同时一定不会匹配另外一个。

这些转义序列,可以在字符簇的内部或外部使用。

如果转义序列后面不加量词修饰,表示匹配所代表的字符簇中的一个字符。例如,/\d/ 表示匹配一个数字。

第四种用法

反斜线还可以用来表示简单的断言。断言指的是假定条件为真。

断言指定了一个必须在特定位置匹配的条件, 它们不会从目标字符串中消耗任何字符。

反斜线可以表示的断言主要包括:

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

上述断言只能在字符簇外部使用。

\A, \Z, \z 断言不同于传统的 ^ 和 $,因为它们永远匹配目标字符串的开始和结尾,而不会受模式修饰符的影响。 它们也不受 PCRE_MULTILINE,PCRE_DOLLAR_ENDONLY 选项的影响。

\Z 和 \z 之间的不同之处在于,当字符串的结束字符是换行符时, \Z 也会将其看做字符串结尾进行匹配,而 \z 只匹配字符串结尾。

Unicode 字符属性

三个额外的转义序列在选用 UTF-8 模式时,用于匹配通用字符类型。

  • \p{xx} 一个有属性 xx 的字符
  • \P{xx} 一个没有属性 xx 的字符
  • \X 一个扩展的 Unicode 字符

xx 代表的属性名用于限制 Unicode 的类别属性。每个字符都有一个这样的确定的属性,通过两个缩写的字母指定。

为了与 perl 兼容, 可以在左花括号 { 后面增加 ^ 表示取反。比如: \p{^Lu} 就等同于 \P{Lu}。

如果通过 \p 或 \P 仅指定了一个字母,它包含所有以这个字母开头的属性。 在这种情况下,花括号的转义序列是可选的。也就是说,\p{L} 等同于 \pL 。

# 匹配 1 个或多个大写字母
preg_match('/\p{Lu}+/', 'ab123ABA456', $matches);
var_dump($matches);

输出结果为:

array(1) { [0]=> string(3) "ABA" }

指定大小写不敏感的匹配模式对这些转义序列不会产生影响,比如, \p{Lu} 始终匹配大写字母。

使用 Unicode 属性来匹配字符并不快, 因为 PCRE 需要去搜索一个包含超过 15000 字符的数据结构。 这就是为什么在 PCRE 中推荐使用传统的转义序列 \d、 \w ,而不推荐使用 Unicode 属性的原因。

字符簇

字符簇是一系列字符的集合。也被称作字符集或字符类。

字符簇的开始标记是 [ , 结束标记是 ] 。如果字符簇中只有一个字符,可以省略方括号。比如,[a] 等价于 a 。

字符簇是正则表达式的重要组成部分。

常见的字符簇写法有:

  • [a] 匹配一个字母 a,等价于 a
  • [abc] 匹配 abc 中的任何一个字母
  • [^abc] 匹配非 abc 字母的任何一个字符
  • [a-z] 匹配任何一个小写字母
  • [A-Z] 匹配任何一个大小字母
  • [a-zA-Z] 匹配任何一个字母
  • [0-9] 匹配任何一个数字
  • [\d] 匹配任何一个数字

由于 . 点号在字符簇中没有特殊含义,所以可以直接使用 [.] 匹配 . 点号。

此外,Perl 支持 POSIX 字符类符号。这种字符类使用 [: 和 :] 闭合。比如,[01[:alpha:]%] 可以匹配 0、1、任意字母、% 中的任意一个。

# 匹配数字、小写字母和下划线中的一个或多个
preg_match('/[0-9[:lower:]_]+/', 'ab_123ABA456', $matches);
var_dump($matches);

输出结果为:

array(1) { [0]=> string(6) "ab_123" }

[:word:] 等价于 \w,[:^word:] 等价于 \W。

模式修饰符

模式修饰符可以对整个正则表达式的匹配模式进行修饰。简称为修饰符。

修饰符一般用于模式的尾部(外部),即置于分隔符之后。

常用的修饰符有:

i (PCRE_CASELESS)

模式中的字母大小写不敏感匹配。

# 匹配任意一个字母,大小写不敏感
/[a-z]/i

等价于

/[a-zA-Z]/

m (PCRE_MULTILINE)

默认情况下,PCRE 认为目标字符串是由单行字符组成的。实际上,目标字符串也可能是多行的。

修饰符 m 可以匹配多行字符串。

如果目标字符串中没有 \n 换行字符,或者模式中没有出现 ^ 或 $,设置 m 修饰符就不会有任何影响。

对于单行字符串,^ 和 $ 分别匹配的是字符串的开头和结尾。

而对于多行字符串,如果设置了 m 修饰符,^ 和 $ 会匹配每一个换行符的尾部(行首)和头部(行尾),另外, 也会匹配整个字符串的开头和结尾。

preg_match(' /^abc$/m', "def\nabc", $matches);
var_dump($matches);

匹配成功,输出结果为:

array(1) { [0]=> string(3) "abc" }

D (PCRE_DOLLAR_ENDONLY)

如果这个修饰符被设置,模式中的元字符美元符号 $ 仅仅匹配目标字符串的末尾。而不会匹配末尾的换行符。

preg_match('/def$/D', "def\n", $matches);
var_dump($matches);

匹配失败,输出结果为:

array(0) { }

如果这个修饰符没有设置,当字符串以一个换行符结尾时,$ 还会匹配字符串末尾的换行符(但不会匹配之前的任何换行符)。

preg_match('/def$/', "def\n", $matches);
var_dump($matches);

匹配成功,输出结果为:

array(1) { [0]=> string(3) "def" }

如果设置了修饰符 m,修饰符 D 就会被忽略。

s (PCRE_DOTALL)

如果设置了这个修饰符,模式中的 . 点号元字符,会匹配所有字符,包含换行符。如果没有这个修饰符,点号就不匹配换行符。

字符簇取反,比如 [^a] 会匹配除了字母 a 以外的任意字符(包含换行符),而不需要依赖于修饰符 s 的设置。

x (PCRE_EXTENDED)

如果设置了这个修饰符,模式中的没有经过转义的或不在字符簇中的空白数据字符总会被忽略。

而且,位于一个未转义的字符簇外部的 # 字符和下一个换行符之间的字符也会被忽略。

A (PCRE_ANCHORED)

如果设置了这个修饰符,模式被强制为【锚定】模式,也就是说约束匹配使其仅从目标字符串的开始位置进行搜索。

这个效果同样可以使用适当的模式构造出来。

U (PCRE_UNGREEDY)

这个修饰符可以逆转量词的【贪婪】模式。

量词具有贪婪性。比如,/.*/ 会匹配 0 个或任意个字符(不包含换行符)。

/.*/U 就是非贪婪模式。

同样,也可以使用模式内修饰符 (?U) 进行设置。还可以在贪婪的量词后使用 ? 符号,将其标记为非贪婪的,例如 /.*?/ 。

在非贪婪模式,通常不能匹配超过 pcre.backtrack_limit 的字符个数。在 php.ini 中,pcre.backtrack_limit 的默认值为 "100000" 。

J (PCRE_INFO_JCHANGED)

内部选项设置 (?J) 修改本地的 PCRE_DUPNAMES 选项,允许子表达式重名。

只能通过内部选项(模式内修饰符)进行设置,模式外部的 J 设置会产生错误。

u (PCRE_UTF8)

这个修饰符会打开一个与 perl 不兼容的附加功能。 模式和目标字符串都被认为是 utf-8 的。

无效的目标字符串会导致 preg_* 函数什么都匹配不到;无效的模式字符串会导致 E_WARNING 级别的错误。

# 匹配任意两个汉字 utf8 编码的正则表达式
$pattern = '/[\x{4e00}-\x{9fa5}]{2}/u';

内部选项设置

内部选项设置是指在模式内部使用修饰符。

可以在模式内部通过一个 perl 选项字符序列来设置模式内部的修饰符。

语法: (?修饰符)

当一个选项在模式的最上级(也就是说不在子表达式中)时, 修饰符会影响模式中的剩余部分。

比如,/ab(?i)c/ 仅仅匹配 abc 和 abC 。

当一个选项处于模式的子表达式中时,修饰符仅仅影响子表达式的剩余部分。

比如,/(a(?i)b)c/ 仅仅匹配 abc 和 aBc 。

此外,还可以在内部选项中使用 - 符号,来取消某种模式。

比如,/a(?-i)bc/i 仅仅匹配 abc 和 Abc 。

内部选项中可用的修饰符有 i、m、s、x、U、X 和 J 。

子表达式

子表达式使得正则表达式匹配起来更加灵活多变。子表达式,也被称为子组或子模式,子组可以相互嵌套。

子组采用小括号标记界定。

在模式(正则表达式)中使用子组,主要有两个方面的作用:

  • 将可选分支局部化。比如,/pa(per|nel)/ 可以匹配 paper 和 panel 。
  • 便于捕获子组的匹配结果。当整个表达式被匹配后,我们也可以单独获取每个子组的匹配结果。第一个子组的下标为 1,第二个子组的下标为 2,以此类推。当然,下标 0 表示的是整个表达式的匹配结果。
$pattern = '/the (red|white) (king|queen)/';
preg_match($pattern, "the red king", $matches);
print_r($matches);

输出结果为:

Array ( [0] => the red king [1] => red [2] => king )

有时,虽然我们采用了子组,但并不想单独捕获某个子组的匹配结果时,可以在子组的左括号后紧跟 ?: 来实现。

$pattern = '/the (?:red|white) (king|queen)/';
preg_match($pattern, "the red king", $matches);
print_r($matches);

输出结果为:

Array ( [0] => the red king [1] => king )

如果需要在非捕获子组的开始位置设置内部选项,为了方便简写, 内部选项的修饰符可以位于 ? 和 : 之间。例如:

(?i:saturday|sunday)
等价于
(?:(?i)saturday|sunday)

子组命名

为了便于区分子组,可以给子组命名。

给子组命名的方式有三种:

  • (?P<name>pattern)
  • (?<name>pattern)
  • (?'name'pattern)

被命名的子组,将会在匹配结果中同时以其名称和顺序(数字下标)出现。

$pattern = '/the (?<sub1>red|white) (king|queen)/';
preg_match($pattern, "the red king", $matches);
print_r($matches);

输出结果为:

Array ( [0] => the red king [sub1] => red [1] => red [2] => king )

多个子组共用同一个数字下标

先来看下面的几个例子:

$pattern = '/((Sat)ur|(Sun))day/';
preg_match($pattern, "Saturday", $matches);
print_r($matches);

// Array ( [0] => Saturday [1] => Satur [2] => Sat )
$pattern = '/((Sat)ur|(Sun))day/';
preg_match($pattern, "Sunday", $matches);
print_r($matches);

// Array ( [0] => Sunday [1] => Sun [2] => [3] => Sun )

上面的正则表达式共有三个子组,我们想让 (Sat) 和 (Sun) 都共用数字下标 1,以便可以使用相同的后向引用数字 1。

如果我们仅仅不捕获第一个子组,看看是什么结果。

$pattern = '/(?:(Sat)ur|(Sun))day/';
preg_match($pattern, "Saturday", $matches);
print_r($matches);

// Array ( [0] => Saturday [1] => Sat )
$pattern = '/(?:(Sat)ur|(Sun))day/';
preg_match($pattern, "Sunday", $matches);
print_r($matches);

// Array ( [0] => Sunday [1] => [2] => Sun )

可以发现,数字下标 1 总是被 (Sat) 占用。

为了修复这个问题,可以使用 ?| 来重用数字下标。

$pattern = '/(?|(Sat)ur|(Sun))day/';
preg_match($pattern, "Saturday", $matches);
print_r($matches);

// Array ( [0] => Saturday [1] => Sat )
$pattern = '/(?|(Sat)ur|(Sun))day/';
preg_match($pattern, "Sunday", $matches);
print_r($matches);

// Array ( [0] => Sunday [1] => Sun )

如此,就实现了嵌套子组中的多个子组共有同一个数字下标。

量词

量词用来指定匹配的个数(次数),也称作限定符。

  • {1} 限定次数为 1
  • {3} 限定次数为 3
  • {3,} 限定次数为 3 次及以上
  • {1,3} 限定次数为 1-3 次
  • ? 限定次数为 0 次或 1 次,等价于 {0,1}
  • * 限定次数为 0 次及以上,等价于 {0,}
  • + 限定次数为 1 次及以上,等价于 {1,}

量词可以对以下的元素的匹配次数进行限定。

  • 单独的字符,可以是经过转义的
  • 元字符
  • 字符簇
  • 后向引用
  • 子组

如果没有明确指定量词,默认匹配次数为 1 次。

贪婪性

默认情况下,量词都是【贪婪】的。

也就是说, 它们会在不导致模式匹配失败的前提下,尽可能多的匹配字符,直到最大允许的匹配次数。

一个贪婪性的典型例子就是匹配注释。

$pattern = '#/\*.*\*/#';
preg_match($pattern, "/* first comment */ not comment /* second comment */", $matches);
print_r($matches);

// Array ( [0] => /* first comment */ not comment /* second comment */ )

这种贪婪性导致它匹配了整个字符串。可以在量词后使用 ? 符号,逆转其贪婪性,将贪婪变为非贪婪。当然,也可以选择使用修饰符 U 来逆转贪婪。

$pattern = '#/\*.*?\*/#';
preg_match($pattern, "/* first comment */ not comment /* second comment */", $matches);
print_r($matches);

// Array ( [0] => /* first comment */ )

后向引用

后向引用,也称作反向引用,在字符簇外部,可以通过 \n 来反向引用之前捕获的某个子组。(数字 n 大于 0)

如果 n 小于 10,\n 总是一个后向引用。

后向引用会直接使用对应的子组在目标字符串中捕获到的内容(子字符串)。

$pattern = '/(sens|respons)e and \1ibility/';
preg_match($pattern, "sense and sensibility", $matches);
print_r($matches);

// Array ( [0] => sense and sensibility [1] => sens )
$pattern = '/(sens|respons)e and \1ibility/';
preg_match($pattern, "response and responsibility", $matches);
print_r($matches);

// Array ( [0] => response and responsibility [1] => respons )

在这个示例中,(sens|respons) 是子组(子表达式),\1 是对该子组捕获到的子字符串的引用。

由于可能会有多达 99 个后向引用, 所有紧跟反斜线后的数字都可能是一个潜在的后向引用。 如果模式在后向引用之后紧接着还是一个数字, 那么必须使用某种分隔符来终结后向引用的语法。 如果设置了修饰符 x, 可以使用空格来做。

例如:

$pattern = '/(ab)cd\1 2/x';
preg_match($pattern, "abcdab2", $matches);
print_r($matches);

// Array ( [0] => abcdab2 [1] => ab )

\g{n} 这种转义系列的后向引用,可以很容易区分反向引用和数字。它使得在后向引用之后再匹配数字变得很直观明朗。

$pattern = '/(ab)cd\g{1}2/';
preg_match($pattern, "abcdab2", $matches);
print_r($matches);

// Array ( [0] => abcdab2 [1] => ab )

如果反向引用之后不需要再匹配数字,\g{n} 中的大括号可以省略。如果 n 为负值,表示相对的反向引用。

$pattern = '/(foo)(bar)\g{-1}/';
preg_match($pattern, "foobarbar", $matches);
print_r($matches);

// Array ( [0] => foobarbar [1] => foo [2] => bar )

上述例子中的模式也可以为:

$pattern = '/(foo)(bar)\g-1/';

或者

$pattern = '/(foo)(bar)\g{2}/';

或者

$pattern = '/(foo)(bar)\g2/';

反向引用也支持使用命名子组的名称。有以下几种写法:

  • (?P=name)
  • \k<name>
  • \k'name'
  • \k{name}
  • \g{name}

下面是一个使用反向引用,颠倒字符串的例子:

$pattern = '/(\w)(\w)(\w)/';
echo preg_replace($pattern, '\3\2\1','abc');

// cba

等价于

$pattern = '/(\w)(\w)(\w)/';
echo preg_replace($pattern, '$3$2$1','abc');

// cba

断言

断言就是一个对当前匹配位置之前或之后的字符的测试, 它不会实际消耗任何字符。

简单的断言有 \b、\B、 \A、 \Z、\z、 ^、$ 等等。

复杂的断言通常会结合子组来实现。

断言分为两种类型:

  • 正向断言(从当前位置进行正向测试)
  • 反向断言(从当前位置进行反向测试)

正向断言

正向断言中的积极断言表示断言此匹配为真。

积极断言以 (?= 开头,消极断言以 (?! 开头。

积极断言

比如, \w+(?=;) 表示匹配一个单词紧跟着一个分号,但匹配结果中不会包含分号。

preg_match('/\w+(?=;)/', 'a_12bcd;', $matches);
print_r($matches);

// Array ( [0] => a_12bcd )

消极断言

比如,foo(?!bar) 表示匹配所有后面没有紧跟 bar 的 foo 字符串。

preg_match('/foo(?!bar)/', 'fooba', $matches);
print_r($matches);

// Array ( [0] => foo )

也就是说,只要目标字符串中的 foo 后面,没有紧跟 bar ,就都会匹配成功。

反向断言

反向断言中的积极断言以 (?<= 开头, 消极断言以 (?<! 开头。

反向断言的内容被严格限制为只能用于匹配定长字符串。但是,如果有多个可选分支, 它们不需要拥有相同的长度。比如 (?<=bullock|donkey) 是允许的, 但是 (?<!dogs?|cats?) 将会引发一个错误。

积极断言

比如,(?<=foo)bar 表示匹配以 foo 开头的 bar。

preg_match('/(?<=foo)bar/', 'foobar', $matches);
print_r($matches);

// Array ( [0] => bar )

消极断言

比如,(?<!foo)bar 用于匹配任何前面不是 foo 的 bar。

preg_match('/(?<!foo)bar/', 'fooobar', $matches);
print_r($matches);

// Array ( [0] => bar )

多个断言(任意顺序)可以同时出现。比如,(?<=\d{3})(?<!999)foo 会匹配前面有三个数字但不是 999 的字符串 foo。

注意, 每个断言会独立应用到对目标字符串该位置点的匹配。 首先它会检查前面的三位都是数字, 然后检查这三位数字不是 999。

多个断言可以以任意复杂度进行嵌套。比如,(?<=(?<!foo)bar)caz 会匹配前面有 bar ,但是 bar 前面没有 foo 的 caz。

(?<=\d{3}…(?<!999))foo 则匹配前面有三个数字字符紧跟 3 个不是 999 的任意字符的 foo。

一次性子组

对于同时有最大值和最小值量词限制的重复项, 在匹配失败后, 紧接着会以另外一个重复次数重新评估是否能使模式匹配。

例如,模式 \d+foo 来匹配目标字符串 123456bar 时,在匹配了 6 个数字后匹配 foo 时失败,这时,匹配器会尝试使 \d+ 只匹配 5 个数字, 只匹配 4 个数字,在最终失败之前依次进行尝试。

一次性子组提供了一种特殊的意义, 当模式的一部分得到匹配后,不再对其进行重新评估。 因此,匹配器在第一次匹配 foo 失败后就能立刻失败。

为了使上述匹配尽快结束,可以将模式改写为 (?>\d+)foo 。

一次性子组以 (?> 开头。

一次性子组对模式的一部分进行了锁定,当它包含一个匹配之后, 会阻止未来模式失败后对它内部的后向回溯。后向回溯在一次性子组中会失效, 其他工作照常进行。

一次性子组仅仅是尽可能多的匹配字符,并使重新评估失效。它不会单独捕获子组匹配的结果。

preg_match('/(?>\d+)abc/', '123456789abc', $matches);
print_r($matches);

// Array ( [0] => 123456789abc )

在这个例子中, (?>\d+) 仅仅会匹配整个数字系列 123456789,匹配成功后,再来匹配 abc,而不会再重新调整匹配的数字的个数。

一次性子组可以包含任意复杂度的字符, 也可以进行嵌套。

当一个模式中的子组自己可以无限重复并且内部有无限重复元素时, 采用一次性子组是避免失败匹配消耗大量时间的唯一途径。

一次性子组可以和反向断言结合使用,来指定在目标字符串末尾的有效匹配。

preg_match('/^(?>.*)(?<=abcd)/', '12355555abcd', $matches);
print_r($matches);

// Array ( [0] => 12355555abcd )

如果模式是 ^.*abcd$, 那么初始的 .* 将首先匹配整个字符串,如果目标字符串不是以 abcd 结尾,就会匹配失败;这样它就会回溯所有的匹配,依次吐出最后 1 个字符,倒数第 2 个字符等等。 从右向左查找整个字符串中的 a, 导致程序不能很好的退出。

然而, 如果模式写作 ^(?>.*)(?<=abcd) , 那么它就不会回溯 .* 这一部分, 它仅仅用于匹配整个字符串。后瞻断言对字符串末尾的后四个字符做了一个测试。 如果它失败,匹配立即失败。对于长字符串来说, 这个模式将会带来显著的处理时间上的性能提升。

条件子组

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

条件子组的两种语法如下:

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

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

条件分为两种:子组匹配条件 和 断言条件。

子组匹配条件

如果在 (condition) 的括号内是数值, 则表示条件是在该数字代表的(之前的)子组得到匹配时满足。

( \( )? [^()]+ (?(1) \) )

模式的第一部分匹配一个可选的左括号,并且如果该字符出现, 设置其为第一个子组的捕获子串。第二部分匹配一个或多个非括号字符。 第三部分是一个条件子组,它会测试第一个子组是否匹配,如果匹配到了, 也就是说目标字符串以左括号开始,条件为TRUE, 那么使用 yes-pattern 也就是这里需要匹配一个右括号。

其他情况下, 既然 no-pattern 没有出现,这个子组就不匹配任何东西。也就是说, 这个模式匹配一个没有括号的或者闭合括号包裹的字符序列。

断言条件

条件子组中的条件,还可以是任意的断言。

为了方便阅读, 增加了一些空白字符,并且在第二行有两个可选分支。

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

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

注释

正则表达式的内部也可以采用注释。

字符序列 (?# 标记开始一个注释直到遇到一个右括号 ) 。

不允许嵌套括号。 注释中的字符不会作为模式的一部分参与匹配。

如果设置了 x 修饰符,字符簇外部的未转义的 # 字符,就表示本行剩余部分都是注释。

<?php
$string = 'test';
echo preg_match('/te(?# comments)st/', $string) . "\n";

echo preg_match('/te#~~~~
st/x', $string) . "\n";

// 1
// 1

递归子组

递归子组指的是利用子组的引用再次匹配子组。

例如,(?1) 可以用于递归匹配第一个子组。

递归子组同样也支持命名子组: (?P>name) 或 (?P&name)。

递归子组语法在子组括号外部使用(无论是子组数字序号还是子组名称), 这个操作就相当于程序设计语言中的子程序。

之前有一个反向引用的例子:

模式 (sens|respons)e and \1ibility 只会匹配 sense and sensibility 和 response and responsibility,但是不匹配 sense and responsibility。

如果用模式 (sens|respons)e and (?1)ibility 来替代, 它就也会匹配 sense and responsibility 。 这种引用方式表示匹配引用的子组。

反向引用,引用的是子组匹配的结果;而递归子组,引用的是子组本身。

猜你喜欢

转载自blog.csdn.net/lamp_yang_3533/article/details/80557614
今日推荐