是否可以找到与单个正则表达式重叠的匹配?

时间:2014-03-01 22:34:16

标签: php regex pcre

这是一个多次执行preg_replace以找到嵌套/重叠匹配的示例:

$text = '[foo][foo][/foo][/foo]';
//1st:   ^^^^^     ^^^^^^
//2nd:        ^^^^^      ^^^^^^
//3rd: fails

do {
    $text = preg_replace('~\[foo](.*?)\[/foo]~', '[bar]$1[/bar]', $text, -1, $replace_count);
} while ($replace_count);

echo $text; //'[bar][bar][/bar][/bar]'

我对结果和行为感到满意。但是,如上例所示,扫描整个字符串3次似乎效率低下。在一次替换中是否有任何正则表达式魔法?

条件:

  • 我不能简单地将~\[(/)?foo]~替换为[$1bar],我需要确保在打开[/foo]标记后有匹配的结束[foo]标记,并将它们替换为一次。它们是否嵌套并不重要。未配对的[foo][/foo]替换。

在JS中,我可以将Regex对象的lastIndex属性设置为匹配的开头,以便从最后一次匹配的开头再次开始匹配。我在PHP中找不到正则表达式替换的任何startIndex选项,并且使用substr() ing也可能效率低下。我已经四处查看PCRE是否会为“在这个位置开始下一场比赛”或类似事件做出让步,但我没有运气。

有更好的方法吗?


在未配对的标签上澄清,给出输入:

[foo][foo][/foo]

我可以使用[bar][foo][/bar][foo][bar][/bar]作为输出。前者是遗留行为。

2 个答案:

答案 0 :(得分:2)

对于这种特定情况,无法使用完整的正则表达式解决方案。

您的解决方案适合匹配配对标签(在常识中):

$pattern = '~\[foo]((?>[^[]++|\[(?!/?foo]))*)\[/foo]~';
$result = $text;
do {
    $result = preg_replace($pattern, '[bar]$1[/bar]', $result, -1, $count);
} while ($count);

另一种只解析字符串一次的方法:

$arr = preg_split('~(\[/?foo])~', $text, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
$stack = array();
foreach ($arr as $key=>$item) {
    if ($item == '[foo]') $stack[] = $key;
    else if ($item == '[/foo]' && !empty($stack)) {
        $arr[array_pop($stack)] = '[bar]';
        $arr[$key] = '[/bar]'; 
    }
}
$result = implode($arr);

第二个脚本的性能与深度无关。

要回答标题问题,是的,可以找到与单个正则表达式重叠的匹配项,但是,您不能使用这种模式执行替换,例如:

$pattern = '~(?=(\[foo]((?>[^[]++|\[(?!/?foo)|(?1))*)\[/foo]))~';
preg_match_all($pattern, $text, $matches);

诀窍是使用前瞻和捕获组。请注意,整个匹配始终为空字符串,这就是为什么不能将此模式与preg_replace一起使用的原因。

答案 1 :(得分:1)

更好的方法是找到结束[/foo]并回溯,直到找到开始[foo][foo(space).*]。将匹配区域替换为其他内容并继续执行,直到找不到结尾。但是使用常规strpos/stripos或普通substr,而不是regex

使用regex可能是可以实现的,但我总是通过定期搜索来做这种事情,因为它也更快。