PHP Conference Japan 2024

递归模式

考虑匹配括号内字符串的问题,允许无限嵌套括号。在不使用递归的情况下,最好的方法是使用一个模式来匹配最多一定深度的嵌套。无法处理任意嵌套深度。Perl 5.6 提供了一个实验性功能,允许正则表达式递归(以及其他功能)。为递归的特定情况提供了特殊项 (?R)。此 PCRE 模式解决了括号问题(假设设置了 PCRE_EXTENDED 选项,以便忽略空格): \( ( (?>[^()]+) | (?R) )* \)

首先,它匹配一个左括号。然后,它匹配任意数量的子字符串,这些子字符串可以是以下两种情况之一:非括号字符序列,或模式本身的递归匹配(即正确括号的子字符串)。最后是一个右括号。

这个特定的示例模式包含嵌套的无限重复,因此在将模式应用于不匹配的字符串时,使用一次性子模式来匹配非括号字符串非常重要。例如,当它应用于 (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa() 时,它会快速产生“不匹配”。但是,如果不使用一次性子模式,匹配将运行很长时间,因为 + 和 * 重复有太多不同的方法可以分割主题,并且在报告失败之前必须测试所有这些方法。

为任何捕获子模式设置的值是从递归的最外层设置子模式值的层。如果上面的模式与 (ab(cd)ef) 匹配,则捕获括号的值为“ef”,这是顶层最后采用的值。如果添加了额外的括号,给出 \( ( ( (?>[^()]+) | (?R) )* ) \),则它们捕获的字符串为“ab(cd)ef”,即顶层括号的内容。如果模式中有多于 15 个捕获括号,则 PCRE 必须在递归期间获取额外的内存来存储数据,它通过使用 pcre_malloc 来实现,并在之后通过 pcre_free 释放它。如果无法获取内存,它只会保存前 15 个捕获括号的数据,因为无法从递归中发出内存不足错误。

(?1)(?2) 等也可用于递归子模式。也可以使用命名子模式:(?P>name)(?&name)

如果递归子模式引用(按数字或按名称)的语法用在它所引用的括号之外,它就像编程语言中的子例程一样工作。前面的示例指出模式 (sens|respons)e and \1ibility 匹配“sense and sensibility”和“response and responsibility”,但不匹配“sense and responsibility”。如果改为使用模式 (sens|respons)e and (?1)ibility,则它也匹配“sense and responsibility”以及其他两个字符串。但是,此类引用必须位于其引用的子模式之后。

主题字符串的最大长度是整数变量可以容纳的最大正数。但是,PCRE 使用递归来处理子模式和无限重复。这意味着可用堆栈空间可能会限制某些模式可以处理的主题字符串的大小。

添加注释

用户贡献注释 8 个注释

horvath at webarticum dot hu
11 年前
使用 (?R) 项,您只能链接到完整模式,因为它等效于 (?0)。您不能使用锚点、断言等,只能检查字符串是否包含有效的层次结构。

这是错误的:^\(((?>[^()]+)|(?R))*\)$

但是,您可以将完整表达式括起来,并将 (?R) 替换为相对链接 (?-2)。这使其可重用。因此,您可以检查复杂的表达式,例如
<?php

$bracket_system
= "(\\(((?>[^()]+)|(?-2))*\\))"; // (可重用)
$bracket_systems = "((?>[^()]+)?$bracket_system)*(?>[^()]+)?"; // (可重用)
$equation = "$bracket_systems=$bracket_systems"; // 等式两侧都必须包含有效的括号系统
var_dump(preg_match("/^$equation\$/","a*(a-(2a+2))=4(a+3)-2(a-(a-2))")); // 输出 'int(1)'
var_dump(preg_match("/^$equation\$/","a*(a-(2a+2)=4(a+3)-2(a-(a-2)))")); // 输出 'int(0)'

?>

您还可以使用“u”修饰符捕获多字节引号(如果您使用 UTF-8),例如
<?php

$quoted
= "(»((?>[^»«]+)|(?-2))*«)"; // (可重用)
$prompt = "[\\w ]+: $quoted";
var_dump(preg_match("/^$prompt\$/u","Your name: »write here«")); // 输出 'int(1)'

?>
emanueledelgrande at email dot it
14 年前
正则表达式中的递归是允许解析具有无限深度嵌套标签的 HTML 代码的唯一方法。
它似乎还不是一种广泛的做法;网络上关于正则表达式递归的内容不多,到目前为止,此手册页面上还没有发布任何用户贡献注释。
我使用复杂的模式进行了多次测试以获取具有特定属性或命名空间的标签,只研究子模式的递归而不是完整模式。
以下是一个示例,它可以为使用递归下降的快速 LL 解析器提供支持(http://en.wikipedia.org/wiki/Recursive_descent_parser

$pattern = "/<([\w]+)([^>]*?) (([\s]*\/>)| (>((([^<]*?|<\!\-\-.*?\-\->)| (?R))*)<\/\\1[\s]*>))/xsm";

在处理平均(x)HTML文档时,`preg_match` 或 `preg_match_all` 函数的性能非常快,这可能促使您选择这种方法而不是传统的 DOM 对象方法,因为后者有很多限制,并且其解决方法通常性能较差。
我发布了一个简短函数(易于转换为面向对象)中的示例应用程序,它返回一个对象数组。

<?php
// 测试函数:
function parse($html) {
// 我将模式分成了两行,以避免 PHP.net 表单中的长行警告:
$pattern = "/<([\w]+)([^>]*?)(([\s]*\/>)|".
"(>((([^<]*?|<\!\-\-.*?\-\->)|(?R))*)<\/\\1[\s]*>))/sm";
preg_match_all($pattern, $html, $matches, PREG_OFFSET_CAPTURE);
$elements = array();

foreach (
$matches[0] as $key => $match) {
$elements[] = (object)array(
'node' => $match[0],
'offset' => $match[1],
'tagname' => $matches[1][$key][0],
'attributes' => isset($matches[2][$key][0]) ? $matches[2][$key][0] : '',
'omittag' => ($matches[4][$key][1] > -1), // 布尔值
'inner_html' => isset($matches[6][$key][0]) ? $matches[6][$key][0] : ''
);
}
return
$elements;
}

// 作为示例的随机 html 节点:
$html = <<<EOD
<div id="airport">
<div geo:position="1.234324,3.455546" class="index">
<!-- 注释测试:
<div class="index_top" />
-->
<div class="element decorator">
<ul class="lister">
<li onclick="javascript:item.showAttribute('desc');">
<h3 class="outline">
<a href="https://php.net/manual/en/regexp.reference.recursive.php" onclick="openPopup()">Link</a>
</h3>
<div class="description">Sample description</div>
</li>
</ul>
</div>
<div class="clean-line"></div>
</div>
</div>
<div id="omittag_test" rel="rootChild" />
EOD;

// 应用程序:
$elements = parse($html);

if (
count($elements) > 0) {
echo
"找到的元素:<b>".count($elements)."</b><br />";

foreach (
$elements as $element) {
echo
"<p>Tpl 节点:<pre>".htmlentities($element->node)."</pre>
标签名:<tt>"
.$element->tagname."</tt><br />
属性:<tt>"
.$element->attributes."</tt><br />
省略标签:<tt>"
.($element->omittag ? 'true' : 'false')."</tt><br />
内部 HTML:<pre>"
.htmlentities($element->inner_html)."</pre></p>";
}
}
?>
Onyxagargaryll
13 年前
这是一种根据字符串分隔符创建多维数组的方法,即我们想要分析...

"some text (aaa(b(c1)(c2)d)e)(test) more text"

...作为多维层级。

<?php
$string
= "some text (aaa(b(c1)(c2)d)e)(test) more text";

/*
* 通过其开始和结束分隔符多维分析字符串
*/
function recursiveSplit($string, $layer) {
preg_match_all("/\((([^()]*|(?R))*)\)/",$string,$matches);
// 遍历匹配项并继续递归拆分
if (count($matches) > 1) {
for (
$i = 0; $i < count($matches[1]); $i++) {
if (
is_string($matches[1][$i])) {
if (
strlen($matches[1][$i]) > 0) {
echo
"<pre>层级 ".$layer.": ".$matches[1][$i]."</pre><br />";
recursiveSplit($matches[1][$i], $layer + 1);
}
}
}
}
}

recursiveSplit($string, 0);

/*

输出:

层级 0: aaa(b(c1)(c2)d)e

层级 1: b(c1)(c2)d

层级 2: c1

层级 2: c2

层级 0: test
*/
?>
匿名
8 年前
sass 解析示例

<?php

$data
= 'a { b { 1 } c { d { 2 } } }';

preg_match('/a (?<R>\{(?:[^{}]+|(?&R))*\})/', $data, $m);
preg_match('/b (?<R>\{(?:[^{}]+|(?&R))*\})/', $data, $m);
preg_match('/c (?<R>\{(?:[^{}]+|(?&R))*\})/', $data, $m);
preg_match('/d (?<R>\{(?:[^{}]+|(?&R))*\})/', $data, $m);

/*
Array (
[0] => a { b { 1 } c { d { 2 } } }
[R] => { b { 1 } c { d { 2 } } }
[1] => { b { 1 } c { d { 2 } } }
)
Array (
[0] => b { 1 }
[R] => { 1 }
[1] => { 1 }
)
Array (
[0] => c { d { 2 } }
[R] => { d { 2 } }
[1] => { d { 2 } }
)
Array (
[0] => d { 2 }
[R] => { 2 }
[1] => { 2 }
)
*/
mzvarik at gmail dot com
4 年前
此正则表达式可用于解析 IF 条件。

$str = '
(IF_MYVAR)My var is printed
(OR_MYVARTWO)My var two is printed
(OR_ANOTHER)if you use OR you don't have to END everytime
(ELSE)Whatever bro(END)

(IF_BLUE)Something (IF_SUPERB)super(END) blue - this is simple IF condition(END)
';

function isCondition($k) {
// 在此处放置您的用户条件
$conds = [];
$conds['BLUE'] = true;
$conds['MYVARTWO'] = true;
$conds['ELSE'] = true; // 始终为真
return $conds[$k];
}

function findConditions($str) {

$pattern = '~ \(if_([^\)]+)\) ((?: (?!\((end|if_)). | (?R) )*+) \(end\) ~xis';

$str = preg_replace_callback($pattern, function($m) {

$k = $m[1];
$v = $m[2];
$v = findConditions($v) ?: $v;

$ors = preg_split('~(?=\((OR_[^\)]+|ELSE))~is', $v);

$v = array_shift($ors); // 主条件

if (isCondition($k)) return findConditions($v);
else {
foreach ($ors as $or) {
list($k, $v) = explode(")", $or, 2);
$k = substr($k, 1);
if (isCondition($k)) return findConditions($v);
}
}
return ''; // 没有匹配的条件
}, $str);
return $str;
};

// 将输出:Whatever bro \n\n Something blue
echo findConditions($str);
jonah at nucleussystems dot com
13 年前
在我的代码中出现了一个意想不到的行为,导致了一个非常难以追踪的错误。它与 `preg_match_all` 的 `PREG_OFFSET_CAPTURE` 标记有关。当您捕获子匹配的偏移量时,它的偏移量相对于其父级给出。例如,如果您在此字符串中递归提取 < 和 > 之间的值

<this is a <string>>

您将得到一个如下所示的数组

数组
(
[0] => 数组
(
[0] => 数组
(
[0] => <this is a <string>>
[1] => 0
)
[1] => 数组
(
[0] => this is a <string>
[1] => 1
)
)
[1] => 数组
(
[0] => 数组
(
[0] => <string>
[1] => 0
)
[1] => 数组
(
[0] => string


[1] => 1
)
)
)

注意最后一个索引处的偏移量是 1,而不是我们预期的 12。解决此问题的最佳方法是使用递归函数遍历结果,并添加父级的偏移量。
Daniel Klein
12 年前
递归子模式中非互斥备选方案的顺序很重要。
<?php
$pattern
= '/^(?<octet>[01]?\d?\d|2[0-4]\d|25[0-5])(?:\.(?P>octet)){3}$/';
?>

您可能期望此模式将匹配任何以点分十进制表示法表示的 IP 地址(例如“123.45.67.89”)。该模式旨在匹配以下范围内的四个八位字节:0-9、00-99 和 000-255,每个八位字节之间用单个点分隔。但是,只有第一个八位字节可以包含 200-255 的值;其余八位字节只能具有小于 200 的值。原因是,如果模式的其余部分失败,则不会回溯到递归中以查找备选匹配。子模式的第一部分将匹配 200-255 中任何八位字节的前两位数字。然后模式的其余部分将失败,因为八位字节中的第三位数字与“\.”或“$”都不匹配。

<?php
var_export
(preg_match($pattern, '255.123.45.67')); // 1 (true)
var_export(preg_match($pattern, '255.200.45.67')); // 0 (false)
var_export(preg_match($pattern, '255.123.45.200')); // 0 (false)
?>

正确的模式是
<?php
$pattern
= '/^(?<octet>25[0-5]|2[0-4]\d|[01]?\d?\d)(?:\.(?P>octet)){3}$/';
?>

请注意,前两个备选方案是互斥的,因此它们的顺序并不重要。但是,第三个备选方案不是互斥的,但现在它只会在前两个失败时才会匹配。

<?php
var_export
(preg_match($pattern, '255.123.45.67')); // 1 (true)
var_export(preg_match($pattern, '255.200.45.67')); // 1 (true)
var_export(preg_match($pattern, '255.123.45.200')); // 1 (true)
?>
horvath at webarticum dot hu
11 年前
下面是一些可重用的模式。我使用了带有“x”修饰符的注释以提高可读性。

您还可以编写一个函数,该函数为指定的括号/引号对生成模式。

<?php

/* 普通圆括号 */
$simple_pattern = "( (?#root pattern)
( (?#text or expression)
(?>[^\\(\\)]+) (?#text)
|
\\((?-2)\\) (?#expression and recursion)
)*
)"
;

$simple_okay_text = "5( 2a + (b - c) ) - a * ( 2b - (c * 3(b - (c + a) ) ) )";
$simple_bad_text = "5( 2)a + (b - c) ) - )a * ( ((2b - (c * 3(b - (c + a) ) ) )";

echo
"Simple pattern results:\n";
var_dump(preg_match("/^$simple_pattern\$/x",$simple_okay_text));
var_dump(preg_match("/^$simple_pattern\$/x",$simple_bad_text));
echo
"\n----------\n";

/* 一些括号 */
$full_pattern = "( (?#root pattern)
( (?#text or expression)
(?>[^\\(\\)\\{\\}\\[\\]<>]+) (?#text not contains brackets)
|
(
[\\(\\{\\[<] (?#start bracket)
(?(?<=\\()(?-3)\\)| (?#if normal)
(?(?<=\\{)(?-3)\\}| (?#if coursed)
(?(?<=\\[)(?-3)\\]| (?#if squared)
(?1)\\> (?#else so if tag)
))) (?#close nested-but-logically-the-some-level subpatterns)
)
)*
)"
;

$full_okay_text = "5( 2a + [b - c] ) - a * ( 2b - {c * 3<b - (c + a) > } )";
$full_bad_text = "5[ 2a + [b - c} ) - a * ( 2b - [c * 3(b - c + a) ) ) }";

echo
"Full pattern results:\n";
var_dump(preg_match("/^$full_pattern\$/x",$simple_okay_text));
var_dump(preg_match("/^$full_pattern\$/x",$full_okay_text));
var_dump(preg_match("/^$full_pattern\$/x",$full_bad_text));
echo
"\n----------\n";

/* 一些括号和引号 */
$extrafull_pattern = "( (?#root pattern)
( (?#text or expression)
(?>[^\\(\\)\\{\\}\\[\\]<>'\"]+) (?#text not contains brackets and quotes)
|
(
([\\(\\{\\[<'\"]) (?#start bracket)
(?(?<=\\()(?-4)\\)| (?#if normal)
(?(?<=\\{)(?-4)\\}| (?#if coursed)
(?(?<=\\[)(?-4)\\]| (?#if squared)
(?(?<=\\<)(?-4)\\>| (?#if tag)
['\"] (?#else so if static)
)))) (?#close nested-but-logically-the-some-level subpatterns)
)
)*
)"
;

$extrafull_okay_text = "5( 2a + ['b' - c] ) - a * ( 2b - \"{c * 3<b - (c + a) > }\" )";
$extrafull_bad_text = "5( 2a + ['b' - c] ) - a * ( 2b - \"{c * 3<b - (c + a) > }\" )";

echo
"Extra-full pattern results:\n";
var_dump(preg_match("/^$extrafull_pattern\$/x",$simple_okay_text));
var_dump(preg_match("/^$extrafull_pattern\$/x",$full_okay_text));
var_dump(preg_match("/^$extrafull_pattern\$/x",$extrafull_okay_text));
var_dump(preg_match("/^$extrafull_pattern\$/x",$extrafull_bad_text));

/*

输出:

Simple pattern results:
int(0)
int(0)

----------
Full pattern results:
int(0)
int(0)
int(0)

----------
Extra-full pattern results:
int(0)
int(0)
int(0)
int(0)

*/

?>
To Top