18、PHP 8.4 新特性 - PCRE2 升级和正则表达式更改

作者: 温新

图书: 【PHP 8.4 新特性】

阅读: 176

时间: 2025-01-18 10:16:02

PHP 8.4 中,PHP 的正则表达式引擎 PCRE2(Perl Compatible Regular Expressions)进行了重要的升级。这些更改主要涉及到一些新的功能、改进以及行为上的调整,从而增强了正则表达式的性能和灵活性

PHP 的正则表达式功能(以 preg_* 函数的形式提供)依赖于 PCRE(Perl 兼容的正则表达式)库。在 PHP 7.3 中,PHP 开始使用 PCRE2。

没有最小数量的量词

在 PHP 8.4 之前,没有指定最小数量的量词被视为无效。而在 PHP 8.4 中,未指定最小数量的量词(例如 /a{,3}/)被视为最小数量为零(即 /a{0,3}/)。

以下代码示例展示了使用正则表达式进行 preg_match 调用,这些正则表达式匹配字符 a 出现零到三次的情况。

preg_match('/a{,3}/', 'aaa'); // 仅在 PHP 8.4 中有效
preg_match('/a{0,3}/', 'aaa'); // 在 PHP 8.4 及更早版本中有效

此语法变化与 Perl 5.34.0 一致。Python 也支持 {,3} 语法,但其他语言,如 JavaScript、Go、Java 等则不支持。

花括号中允许空格

PHP 8.4 允许在量词的花括号对之间、以及分隔数量的逗号两侧使用空格和水平制表符字符。这种语法与 Perl 不兼容,但 ECMAScript 支持这种语法。

以下代码示例展示了在 PHP 8.4 中允许使用空格和制表符的量词语法:

preg_match('/a{ 5,10 }/',    'aaaaaaa'); // 仅在 PHP 8.4 中有效
preg_match('/a{5 ,10}/',     'aaaaaaa'); // 仅在 PHP 8.4 中有效
preg_match('/a{ 5, 10 }/',   'aaaaaaa'); // 仅在 PHP 8.4 中有效
preg_match('/a{ 5, 10   }/', 'aaaaaaa'); // 仅在 PHP 8.4 中有效

在 PHP 8.4 之前和 PCRE2 10.43 之前,上述正则表达式将不被视为有效的量词,而仅作为字符串字面量进行匹配。

Unicode 15 更新

PHP 8.4 内置的 PCRE2 现在支持 Unicode 15。除了 Unicode 15 中新增的表情符号和字形更新外,还包括对新的 Unicode 字符类的支持。

例如,随着 Unicode 15 的更新,新增的 Unicode 脚本现在可以作为命名字符类在正则表达式中使用。Unicode 15 添加了 Kawi(U11F00-11F5F)和 Nag Mundari(U1E4D0-1E4FF)脚本,这意味着它们可以在正则表达式中使用:

preg_match('/\p{Kawi}/u', 'abc');
preg_match('/\p{Nag_Mundari}/u', 'abc');

这些更新使得在正则表达式中使用 Unicode 15 中的新字符集变得更加便捷。

在 PHP 8.4 之前的版本中,使用 Unicode 15 中的新字符类会导致警告,因为这些字符类对 PCRE2 来说是未知的。

例如:

preg_match(): Compilation failed: unknown property after \P or \p at offset ...

在旧版本的 PHP 中,仍然可以匹配这些字符和表情符号,但必须使用字符范围而不是命名字符类来定义正则表达式:

preg_match('/\p{Kawi}/u', 'abc');
// 等价于:
preg_match('/[\x{11F00}-\x{11F5F}]/u', 'abc');

preg_match('/\p{Nag_Mundari}/u', 'abc');
// 等价于:
preg_match('/[\x{1E4D0}-\x{1E4FF}]/u', 'abc');

此外,Unicode 15 更新还带来了对现有字符类的更多改动、新的表情符号以及一个新的 ZWJ(Zero Width Joiner)模式,用于表情符号组合。ZWJ 使得多个表情符号可以组合在一起显示为一个新的复合符号,例如皮肤色调修改符号和多个表情符号的组合。

<?php

declare(strict_types=1);

$pattern = '/\p{L}+/u';  // 匹配任意 Unicode 字母字符
$string = "你好,世界";
preg_match_all($pattern, $string, $matches);
print_r($matches);

输出

Array
(
    [0] => Array
        (
            [0] => 你好
            [1] => 世界
        )

)

Unicode 模式下的正则表达式 \w

在 PHP 8.4 之前,使用 /\w/u 的字符类等同于 /[\p{L}\p{N}_]/u。这意味着 \w\p{L}(Unicode 字母字符)、\p{N}(任何脚本中的数字字符)和下划线(_)的简写。

在 PHP 8.4 及以后的版本中,\w 还包括了 \p{Mn}(非间距标记)和 \p{Pc}(连接标点)。因此,\w 变得等同于 /[\p{L}\p{N}_\p{Mn}\p{Pc}]/u。这种新行为与 Perl 的行为一致。

根据 Unicode 15,Mn 字符类别包含 1,839 个条目,Pc 字符类别包含 10 个条目。这意味着,/w/u 现在匹配 1,849 个额外的字符,这可能会对现有的正则表达式产生较大的影响。

例如:

preg_match('/\w/u', "\u{0300}"); // PHP < 8.4: 不匹配
preg_match('/\w/u', "\u{0300}"); // PHP >= 8.4: 匹配

在 PHP 8.4 及更高版本中,\w 现在会匹配更多的字符,包括一些原本不匹配的 Unicode 非间距标记字符(如变音符号)。这可能会影响现有的正则表达式行为,尤其是在需要精确匹配字符范围时。

不区分大小写的限制修饰符支持

作为 PCRE2 10.43 更新的一部分,PHP 8.4 引入了 "caseless restrict"(不区分大小写限制)修饰符(PCRE2_EXTRA_CASELESS_RESTRICT),可以在正则表达式中使用,也可以作为一个附加标志,作用于整个表达式。

当应用此修饰符时,它会阻止在 ASCII 和非 ASCII 字符之间进行不区分大小写的匹配。例如,Kelvin 符号(K,"\u{212A}")和英语字母 K 可以与小写字母 k 互换匹配,在 Unicode 正则表达式中如下所示:

preg_match('/k/iu', "K"); // 匹配
preg_match('/k/iu', "k"); // 匹配
preg_match('/k/iu', "\u{212A}"); // 匹配

PHP 8.4 引入的 "caseless restrict" 模式会阻止 ASCII 和非 ASCII 字符之间的不区分大小写匹配。启用此模式的方法是在不区分大小写匹配开始的位置添加 (?r),而 (?-r) 则禁用此功能。

preg_match('/(?r)k/iu', "K"); // 匹配
preg_match('/(?r)k/iu', "k"); // 匹配
preg_match('/(?r)k/iu', "\u{212A}"); // 不匹配

此模式影响 Unicode 代码点大于 U+0080 的字符,这些字符有大小写折叠规则,定义在 Unicode 大小写折叠标准中。根据 Unicode 15,目前符合此标准的字符包括:

字符 ASCII 基本等价字符
U+212A - 开尔文符号 (K) 006B - 拉丁字母大写 K (K)
U+017F - 拉丁小写长 S (ſ) 0073 - 拉丁小写字母 S (s)

这意味着在 "caseless restrict" 模式下,只有这些特定的字符(如 K)会互相匹配,而其他字符(如 k)则不会互相匹配。

在 PHP 8.4 中,使用 "caseless restrict" 模式进行整个正则表达式匹配可以通过引入新的 "r" 修饰符来实现:

preg_match('/\x{212A}/iu', "K"); // 匹配
preg_match('/\x{212A}/iur', "K"); // 不匹配

在上面的例子中,/iu 表示不区分大小写匹配,而 /iur 表示启用了 "caseless restrict" 模式。在 PHP 8.4 中,当你在正则表达式中使用 r 修饰符时,它会启用整个正则表达式的 caseless-restrict 模式,这意味着 ASCII 和非 ASCII 字符之间的大小写不再互相匹配。

对于 PHP 8.4 之前的版本,尝试使用 "caseless restrict" 修饰符会触发 PHP 警告:

Compilation failed: unrecognized character after (? or (?-

同样,由于 "r" 修饰符仅在 PHP 8.4 及更高版本中可用,在旧版本中使用该修饰符也会导致 PHP 警告:

Warning: preg_match(): Unknown modifier 'p'

因此,在使用 PHP 8.4 或更高版本时,确保只有在支持此修饰符的环境中使用它,以避免警告或错误。

可变长度的回溯断言支持

PCRE2 10.43 引入了对可变长度回溯断言的支持,只要指定了最大长度。这意味着 PHP 8.4 支持像以下这样的正则表达式:

preg_match('/(?<=Hello{1,5}) world/', 'Hello world'); // 匹配
preg_match('/(?<=Hello{1,5}) world/', 'Hellooooo world'); // 匹配
preg_match('/(?<=Hello{1,5}) world/', 'Helloooooo world'); // 不匹配

在 PHP 8.3 及更早版本中,回溯断言(包括正向回溯和负向回溯)是受支持的,但正则表达式中不能包含量词(如 {x,y}?*)。如果这样做,会导致编译失败:

Warning: preg_match(): Compilation failed: lookbehind assertion is not fixed length

在 PHP 8.4 及更高版本中,允许使用可变长度的回溯断言,前提是量词必须具有上限。这意味着像 (?<=a?)(?<=a{1,20}) 这样的回溯断言是允许的。

然而,*+ 不能用于回溯断言中,因为它们没有定义上限。使用这些量词会导致编译错误:

Warning: preg_match(): Compilation failed: length of lookbehind assertion is not limited

另外,对于回溯量词的上限,也有一个限制:上限不能超过 255。超过此限制(例如 (?<=a{1,256}))将导致编译错误:

Warning: preg_match(): Compilation failed: branch too long in variable-length lookbehind assertion

命名捕获组标签长度增加至 128

PCRE2 10.44 增加了命名捕获组标签的最大长度。在 PHP 8.4 之前,命名捕获组标签的最大长度为 32 个字符。

例如,在 PHP 8.4 及更高版本中,命名捕获组标签可以最多为 128 个字符长:

preg_match('/(?<mylabel1234567890123456789012345>a+)/');

在 PHP 8.4 及更高版本中,标签(例如上面的 mylabel1234567890123456789012345)可以最多为 128 个字符。

在 PHP 8.4 之前,使用长于 32 个字符的命名捕获组标签,或者在 PHP 8.4 及更高版本中使用长于 128 个字符的标签,将导致编译错误:

Compilation failed: subpattern name is too long (maximum 32 code units) at offset 36 ...

向后兼容性影响

PCRE2 库是 PHP 源代码的一部分,因此无法将这些更改引入较旧版本的 PHP。

一些功能(如 Kawi 和 Nag Mundari 脚本的命名字符类)可以通过指定它们的 Unicode 范围在旧版本的 PHP 中使用,但大部分新功能无法回溯到旧版本的 PHP。

您可以使用 PCRE_VERSION 常量来提供捆绑的 PCRE2 版本,这在根据可用性执行条件 preg_* 调用时非常有用。

请登录后再评论