PHP 正则表达式:如何切割嵌套模式?

PHP regexp: how to cut nested patterns?

我有一个很愚蠢的问题,让我困惑了一段时间...
我想解析一些文本,格式如下:

CUT-FROM-A ...
CUT-FROM-B ...
CUT-TO ...
CUT-TO
apple
CUT-FROM-C ...
CUT-TO
orange

在这个例子中,我想提取 'fruits',忽略从 CUT-FROM-X 到相应的 TO 的所有内容。 'corresponding' 我的意思是 "from inside to outside",或者如果它更清楚,请尝试用开括号替换任何 CUT-FROM-A,并将任何 CUT-TO 替换为闭括号:然后,我想忽略括号内的内容,包括括号。
我希望这很清楚,但恐怕不是...:-(
我想这里的主要困难是 'closing brackets' 都有相同的签名,所以不能轻易地与相关的开瓶器相关联...

我试过这样的(不贪心):

$output_text = preg_replace("/CUT-FROM-.*?TO/s", "", $input_text);

但这会在输出中留下第二个 CUT-TO...

像这样的东西(贪心):

$output_text = preg_replace("/CUT-FROM-.*TO/s", "", $input_text);

但这吃掉了第一个 'fruit'... :-(

This 是我对 regex101 的测试。

任何人都可以给我一些启示吗?

想一想,您可以处理与您想要的上下文匹配的每一行,而不是替换。

preg_match_all('~^(?!.*CUT-(?:FROM|TO)).+$~mi', $text, $matches);
var_dump($matches[0]);

输出

array(2) {
  [0]=> string(5) "apple"
  [1]=> string(6) "orange"
}

您可以使用单个 regex 来完成此操作,但您可以使用使用小型 regex 来完成较小任务的简单脚本来更好地完成此操作。

思路:逐行解析文本,使用regex识别行类型。在每个 'CUT-FROM' 行上,将信息(行本身或其他内容)添加到堆栈(使用 array_push())。在每 'CUT-TO' 行从堆栈中删除顶部元素(使用 array_pop().

根据需要处理其他行。例如,如果您需要忽略 'CUT-FROM' 和相应的 'CUT-TO' 行之间的行,您需要检查堆栈是否为空以了解您在一对中。如果堆栈为空,则所有 'CUT-FROM' 都与 'CUT-TO' 行配对,并且您正在解析任何外壳之外的行。

这种方法还为您提供了一种检测和处理 (ignore/fix/report/whatever) 输入文本中错误的好方法。

示例程序:

text = <<< END_TEXT
CUT-FROM-A ...
ignore this,
CUT-FROM-B ...
this,
CUT-TO ...
and this
CUT-TO
apple
CUT-FROM-C ...
CUT-TO
orange
END_TEXT;

$lines = explode("\n", $text);


$stack = array();
foreach ($lines as $i => $line) {
    // Check if it's a 'CUT-FROM-' line
    if (preg_match('/^CUT-FROM-/', $line)) {
        array_push($stack, $line);
        continue;
    }

    // Check if it's a 'CUT-TO' line
    if (preg_match('/^CUT-TO/', $line)) {
        if (array_pop($stack) === NULL) {
            // an unpaired 'CUT-TO' was found
            echo("An unpaired 'CUT-TO' was found on line ".($i + 1).". Will ignore it.\n");
        }
        continue;
    }


    // A regular line
    if (count($stack) > 0) {
        // inside a (CUT-FROM, CUT-TO) pair
        // count($stack) tells how many pairs are around this item

        // ignore it

    } else {
        // outside any pair
        echo ($line."\n");
    }
}

// Check if all the 'CUT-FROM' lines were closed
if (count($stack) > 0) {
    echo('Found that '.count($stack)." 'CUT-TO' lines are missing at the end of processing.\n");
}

由于您要求的是正则表达式解决方案,因此可读的递归正则表达式为:

(?(DEFINE)
  (?<cut>
    ^CUT-FROM-
    (?&content)*?
    ^CUT-TO
  )

  (?<content>
    (?: (?!CUT-(?:FROM-|TO)) . )++
    | (?&cut)
  )
)

(?&cut)

Demo

smx 选项一起使用。这匹配您要忽略的所有内容,因此您可以将其替换为空字符串。语法 (?&something) 表示 递归到 something,它与 \g<something>.

相同

这是一个更紧凑的版本,其功能基本相同:

^CUT-FROM-
(?:(?:(?!CUT-(?:FROM-|TO)) . )++ | (?R))*?
^CUT-TO

Demo

在此版本中,(?R) 表示递归整个模式。它仍然使用 smx 选项。单行版本(没有 x)将是:

(?sm)^CUT-FROM-(?:(?:(?!CUT-(?:FROM-|TO)).)++|(?R))*?^CUT-TO

但我建议不要做这样的事情。更喜欢带有 (?(DEFINE) ... ) 的版本以提高可读性。