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

is it possible to find overlapping matches with a single regex?

提问人:Fabrício Matté 提问时间:3/2/2014 最后编辑:Fabrício Matté 更新时间:3/2/2014 访问量:542

问:

下面是一个示例,它多次执行以查找嵌套/重叠的匹配项: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 中,我可以将正则表达式对象的属性设置为匹配的开头,以便它从最后一个匹配的开始再次开始匹配。我在 PHP 中找不到任何替换正则表达式的选项,并且使用 ing 也可能效率低下。我环顾四周,PCRE是否会有“在这个位置开始下一场比赛”或类似的东西,但我没有运气。lastIndexstartIndexsubstr()

有没有更好的方法?


为了澄清未配对的标签,给定输入:

[foo][foo][/foo]

我对任何一个或作为输出都很好。前者是遗留行为。[bar][foo][/bar][foo][bar][/bar]

PHP 正则表达 PCRE

评论

0赞 HamZa 3/2/2014
根据我的经验,我会说这是不可能的。但我会很高兴看到有人让我眼花缭乱!同时,这里有一个更复杂的正则表达式,它与适当的嵌套标签相匹配!
0赞 Casimir et Hippolyte 3/2/2014
首先,你的代码没有像你想象的那样工作,试试用字符串[foo][foo][/foo]
0赞 Fabrício Matté 3/2/2014
@CasimiretHippolyte我相信它有效。中间的未配对不会被替换,对吧?这就是我所期望的。哦,我对“配对”部分不是很清楚,第一个打开的标签和它后面的第一个关闭标签是一个很好的配对。[foo]

答:

1赞 CodeAngry 3/2/2014 #1

更好的方法是找到结束并回溯,直到找到开始或。用其他东西替换匹配区域并继续这样做,直到找不到结局。但是用普通的或普通的旧,不是.[/foo][foo][foo(space).*]strpos/stripossubstrregex

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

评论

0赞 HamZa 3/2/2014
我不明白这个答案的本质。请记住,最终目标是在一个正则表达式(没有循环)中做到这一点。
1赞 CodeAngry 3/2/2014
@HamZa 他问。你注意到了......右?就在最后。Is there a better approach?
0赞 HamZa 3/2/2014
Is there any regex magic to do this in a single replace?注意?
0赞 CodeAngry 3/2/2014
@HamZa 没有(afaik),所以我给了他一个更快的解决方案。很酷,对吧?
1赞 Fabrício Matté 3/2/2014
我实际上有一个替代使用 and ,尽管我想在我回到 'ing 之前看看 Regex 是否可行。strpossubstr_replacesubstr_replace
2赞 Casimir et Hippolyte 3/2/2014 #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一起使用的原因。

评论

0赞 Fabrício Matté 3/2/2014
+1 感谢您的出色选择。我会对它们进行一些分析并报告。
0赞 Fabrício Matté 3/2/2014
我看到您已经编辑了删除递归的答案,因为在我的情况下这是不必要的。那么,只是一个更快的替代方案吗?(?>[^[]++|\[(?!/?foo]))*.*?
0赞 Fabrício Matté 3/2/2014
我可以看到你正在使用所有格量词和仅一次子模式来避免回溯,但会做任何回溯吗?.*?
0赞 Casimir et Hippolyte 3/2/2014
惰性量词必须在每个位置检查以下子模式是否与字符串匹配。如果使用贪婪量词,则正则表达式引擎不必进行这些检查并匹配所有可能的内容(并返回,直到以下子模式匹配)。但是由于我使用否定的字符类,因此该组只能匹配到其他标签(关闭或打开)。由于我使用所有格量词和原子组,因此如果找不到结束标记,模式将更快地失败。.*?
0赞 Casimir et Hippolyte 3/2/2014
.*?不确保两个标签之间没有其他标签。