编程

PHP 8.4: PCRE2 升级及正则表达式更改

523 2024-04-07 01:04:00

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

PHP 一直在缓慢地保持微小的 PCR E更新,如 2021 年的 PCRE2 10.39 和2022年的 PCRE2 10.40。然而,PCRE2 10.43 带来了一些重大变化,包括影响其支持的正则表达式语法的变化。

PCRE2 库包含在 PHP 源代码树中,因此编译时依赖关系中不涉及任何更改。

PCRE2 升级有一些更改,这些更改可能与现有的正则表达式不兼容,或者与其他风格的正则表达式引擎不兼容。

正则表达式语法更改

以下是作为 PCRE2 10.43 更新的一部分对正则表达式语法的更改。这些更改将在 PHP 8.4 中生效,因为这是有效地带来 PCRE2 10.43 更新的版本。

没有最小数量的量化器

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

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 允许在数量词的大括号的前后以及逗号周围使用空格和水平 tab 符来分隔数量。这与 Perl 不兼容,但 ECMAScript 支持这种语法。

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's 捆绑的 PCRE2 现在支持 Unicode 15。除了新的表情符号 Emoji 和 Unicode 15 中的字形更新外,这还包括对新的 Unicode 字符类的支持。

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

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

在 PHP 8.4 之前的版本,这会带来警告,因为 PCRE2 并不知晓这些字符类。

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

老版的 PHP 也能匹配这些字符和 emoji,不过不是使用名称匹配,正则表达式必需定义范围:

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 更新带来了更多对现有字符类的改变,新的 Emoji 以及 一个用于 Emoji 组合的 ZWJ 模式。

Unicode 模型中的正则 \w

PHP 8.4 之前,使用 /\w/u 字符类相当于 /[\p{L}\p{N}_]/u。也就是说 \w 是字符类 \p{L} (Unicode "字母(letter)" 字符点)、\p{N} (数值字符)以及下划线 (_) 的快捷方式。

PHP 8.4 及此后,\w 另外包含了 \p{Mn}(非空格标记)和\p{Pc} (连接器标点符号)。这使得 \w 相当于 /[\p{L}\p{N}_\p{Mn}\p{Pc}]/u。这一新行为与 Perl 匹配。

截至 Unicode 15,Mn 字符类别包含 1839个 条目,Pc 类别包含 10 个条目。这也可能对现有的正则表达式产生更大的影响,因为 /w/u 现在匹配 1849 个额外的字符。

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

支持 Caseless Restrict 修改器

作为 PCRE2 10.43 update 更新的一部分,PHP 8.4 可以在常规表达式中利用 "caseless restrict" (PCRE2_EXTRA_CASELESS_RESTRICT) 修改器并将其作为应用到整个表达式的额外标记。

当使用时,它阻止它会阻止 ASCII 和非 ASCII 字符之间的匹配。例如,开尔文符号(, "\u{212A}")和英文字母 K 可以在 Unicode 正则中与 K(英文简单字母 k)互换匹配:

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

PHP 8.4 引入了 "caseless restrict" 模式,它阻止不区分大小写(/i)在 ASCII 和非 ASCII 字符之间的匹配。该模式通过将 (?r) 放置在应该开始匹配 caseless 的位置启用。(?-r) 禁用了不区分大小写匹配.

preg_match('/(?r)k/iu', "K"); // Matches
preg_match('/(?r)k/iu', "k"); // Matches
preg_match('/(?r)k/iu', "\u{212A}"); // Does NOT Match

大于 0080 的 Unicode 代码点有大小写折叠规则。它们在 Unicode 大小写折叠标准中进行了定义。截至 Unicode 15,匹配该条件的代码点仅有:

CharacterASCII base equivalent
U+212A - KELVIN SIGN (K)006B - Latin Capital Letter K (K)
U+017F - LATIN SMALL LETTER LONG S (ſ)0073 - Latin Small Letter S (s)

要在 caseless-restrict 模式下实现整个正则,PHP 8.4 支持了新的 "r" 标志:

preg_match('/\x{212A}/iu', "K"); // Matches
preg_match('/\x{212A}/iur', "K"); // Does NOT match

在 PHP 8.4 之前的版本使用 caseless-restrict 修改器会发出警告:

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

因为 "r" 标记只在 PHP 8.4 之后的版本可用,它同时会产生 PHP 警告:

Warning: preg_match(): Unknown modifier 'p'

限制长度的可变长度后顾支持

PCRE2 10.43 支持可变长度后顾(lookbehind)断言,只要有最大长度。这意味者 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}? 或者 *)。这会在 PHP 8.3 及之前的版本中导致编译失败。

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

PHP 8.4 及其之后,允许使用可变长度后顾(lookbehind)断言,但限定符必需有上限。这意味着像 (?<=a?)(?<=a{1,20}) 这样的断言是允许的。

不过,不支持 *+,因为它们没有定义上限。使用它们会产生编译错误:

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

还有一个限制是,后顾量词的上限必需小于或等于 255。超过该限制(e.g. (?<=a{1,256}) )将会导致编译错误:

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

向后兼容性影响

PCRE2 库是是 PHP 源代码树的一部分,无法将这些变更带回到老版本 PHP 中。

有些功能(比如 KawiNag Mundari 脚本命名字符类)可用通过指定其 Unicode 范围,在老版本中使用。不过,主要的新功能无法带到旧版 PHP 中。

使用PCRE_VERSION 常量可以提供捆绑的 PCRE2 版本 ,这对根据可用性运行条件性 preg_* 调用时可能有用。