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 則註解

20
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)'

?>
11
emanueledelgrande at email dot it
14 年前
正規表示式中的遞迴是唯一允許剖析具有不確定深度巢狀標籤的 HTML 程式碼的方法。
看來這還不是普及的做法;網路上關於正規表示式遞迴的內容不多,而且到目前為止,本手冊頁面上沒有發布任何使用者貢獻的註解。
我使用複雜的模式進行了幾次測試,以取得具有特定屬性或命名空間的標籤,只研究子模式的遞迴,而不是完整的模式。
這是一個範例,可能會使用遞迴下降(http://en.wikipedia.org/wiki/Recursive_descent_parser)為快速 LL 剖析器提供動力

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

在一般 (x)HTML 文件上呼叫 preg_match 或 preg_match_all 函數的效能相當快,這可能會促使您選擇此方法,而不是傳統的 DOM 物件方法,後者有很多限制,而且其變通方法通常效能不佳。
我會在簡短的函數中發布範例應用程式(很容易轉換為 OOP),它會傳回物件陣列

<?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.dev.org.tw/manual/en/regexp.reference.recursive.php" onclick="openPopup()">連結</a>
</h3>
<div class="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 />
Omittag:<tt>"
.($element->omittag ? 'true' : 'false')."</tt><br />
內部 HTML:<pre>"
.htmlentities($element->inner_html)."</pre></p>";
}
}
?>
7
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
*/
?>
3
Anonymous
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 }
)
*/
0
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; // 永遠為 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);
0
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
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)
?>
-1
horvath at webarticum dot hu
11 年前
以下是一些可重複使用的模式。我使用带有 'x' 修飾符的註解來提高人類可讀性。

您還可以編寫一個函數,該函數會為指定的括號/引號對產生模式。

<?php

/* 一般圓括號 */
$simple_pattern = "( (?#根模式)
( (?#文字或表達式)
(?>[^\\(\\)]+) (?#文字)
|
\\((?-2)\\) (?#表達式和遞迴)
)*
)"
;

$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
"簡單模式結果:\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 = "( (?#根模式)
( (?#文字或表達式)
(?>[^\\(\\)\\{\\}\\[\\]<>]+) (?#文字不包含括號)
|
(
[\\(\\{\\[<] (?#起始括號)
(?(?<=\\()(?-3)\\)| (?#如果是一般括號)
(?(?<=\\{)(?-3)\\}| (?#如果是大括號)
(?(?<=\\[)(?-3)\\]| (?#如果是方括號)
(?1)\\> (?#否則如果是標籤)
))) (?#關閉巢狀但邏輯上相同層級的子模式)
)
)*
)"
;

$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
"完整模式結果:\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 = "( (?#根模式)
( (?#文字或表達式)
(?>[^\\(\\)\\{\\}\\[\\]<>'\"]+) (?#文字不包含括號和引號)
|
(
([\\(\\{\\[<'\"]) (?#起始括號)
(?(?<=\\()(?-4)\\)| (?#如果是一般括號)
(?(?<=\\{)(?-4)\\}| (?#如果是大括號)
(?(?<=\\[)(?-4)\\]| (?#如果是方括號)
(?(?<=\\<)(?-4)\\>| (?#如果是標籤)
['\"] (?#否則如果是靜態)
)))) (?#關閉巢狀但邏輯上相同層級的子模式)
)
)*
)"
;

$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
"超完整模式結果:\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));

/*

輸出:

簡單模式結果:
int(0)
int(0)

----------
完整模式結果:
int(0)
int(0)
int(0)

----------
超完整模式結果:
int(0)
int(0)
int(0)
int(0)

*/

?>
To Top