【问题标题】:recursively parse custom markup递归解析自定义标记
【发布时间】:2014-01-03 12:30:40
【问题描述】:

我必须处理已经存在的自定义标记语言(这很丑陋,但不幸的是无法更改,因为我正在处理旧数据并且它需要与旧应用保持兼容)。

我需要解析命令“范围”,并根据用户采取的操作将数据中的这些“范围”替换为其他内容(HTML 或 LaTeX 代码)或从输入中完全删除这些“范围”。

我目前的解决方案是在循环中使用preg_replace_callback(),直到没有匹配项为止,但对于大型文档来说它非常慢。 (即 57 KB 文档中 394 次替换大约需要 7 秒)

递归正则表达式对于这项任务似乎不够灵活,因为我需要访问所有匹配项,即使在递归中也是如此。

问题:如何提高解析性能?

正则表达式可能会被完全删除——它们不是必需的,但我唯一能想到的。

注意:下面的代码示例已大大减少。 (SSCCE)实际上有许多不同的“类型”范围,闭包函数根据操作模式做不同的事情。 (从 DB 中插入值、删除整个范围、转换为另一种格式等。)请记住这一点!

我目前正在做的示例:

<?php
$data = <<<EOF
some text 1
begin-command
    some text 2
    begin-command
        some text 3
    command-end
    some text 4
    begin-command-if "%VAR%" == "value"
        some text 5
        begin-command
            some text 6
        command-end
    command-end
command-end

EOF;

$regex = '~
    # opening tag
    begin-(?P<type>command(?:-if)?)
    # must not contain a nested "command" or "command-if" command!
    (?!.*begin-command(?:-if)?.*command(?:-if)?-end)
    # the parameters for "command-if" are optional
    (?:
        [\s\n]*?
        (?:")[\s\n]*(?P<leftvalue>[^\\\\]*?)[\s\n]*(?:")
        [\s\n]*
        # the operator is optional
        (?P<operator>[=<>!]*)
        [\s\n]*
        (?:")[\s\n]*(?P<rightvalue>[^\\\\]*?)[\s\n]*(?:")
        [\s\n]*?
    )?
    # the real content
    (?P<content>.*?)
    # closing tag
    command(?:-if)?-end
 ~smx';

$counter = 0;
$loop_replace = true;
while ($loop_replace) {
    $data = preg_replace_callback($regex, function ($matches) use ($counter) {
        global $counter;
        $counter++;
        return "<command id='{$counter}'>{$matches['content']}</command>";
    }, $data, -1, $loop_replace);
}
echo $data;

【问题讨论】:

  • 提高性能(明显)很可能意味着使用纯字符串操作,而不是正则表达式。
  • @BartKiers 完全同意。但是,考虑到文档结构(嵌套)的复杂性和要执行的不同任务,如何处理?这就是为什么我问这个问题,实际上。我不知道如何继续最好。

标签: php parsing recursion


【解决方案1】:

您对正则表达式第 4 行的展望:

(?!.*begin-command(?:-if)?.*command(?:-if)?-end)

每次遇到它时都必须读取到文件末尾(使用正在使用的修饰符)

让你的 .* 变得懒惰可能会让你在这些大文件上获得一点性能提升:

(?!.*?begin-command(?:-if)?.*?command(?:-if)?-end)

如果 (?:-if)?总是会在 begin-command 之后出现,你可以在那里摆脱它,会变成这样:

(?!.*?begin-command.*?command(?:-if)?-end)  

【讨论】:

  • 很好的发现和合理的改进。但测试表明这种变化会使正则表达式的执行速度更慢! (慢约 100 毫秒,在空闲系统上通过 5 次捕获验证)
  • 哎呀我忘了放?在最后一个命令的 .* 之后。因此,如果您按原样使用它,它将无法正常工作(更新),可能会有所不同。我想如果命令之间有非常大的代码块,这实际上可能会影响性能。
  • 对于采样捕获,我使用了您的第一个示例,因为我只想衡量贪婪与不贪婪。 (导致实际示例数据的计算时间增加了约 100 毫秒)...当我使用您的最终正则表达式时,情况变得更糟:慢了约 300-400 毫秒。
  • 我想知道为什么会更慢。从理论上讲,您应该是对的,使用不贪婪的量词应该会大大提高性能。
  • 有趣。您的示例标记文件有多大?在小样本集(如您的示例中提供的样本)上,差异可能可以忽略不计或稍慢一些。但是当我成长时,比如将根开始命令下的所有内容复制 40 次,“懒惰”版本会明显更快。
【解决方案2】:

我现在已经完全删除了用于解析的正则表达式。我意识到,实际上原始输入可以看作是某种奇怪表示形式的 XML 标记树。

我现在不使用正则表达式,而是执行以下操作:

  1. 用文本表示(使用 XML 实体)替换所有可以解释为 XML 的内容
  2. 用相应的 XML 标记替换所有 begin-command ... command-end
    (注意实际上有几个不同的命令)
  3. 让真正的解析器 (XML DOM) 处理标记树
  4. 递归地遍历 DOM
  5. 对于每个节点,根据操作模式执行适当的操作

这看起来很难看,但我真的不想编写自己的解析器 - 在我用于提高速度的有限时间内,这似乎有点“矫枉过正”。哦,男孩,这仍然非常快 - 比 RegExp 解决方案快得多。令人印象深刻的是,当您考虑将原始输入转换为有效 XML 并返回的开销时。

“极快”是指现在只需约 200 毫秒即可处理一个文档,而以前需要 5-7 秒来解析多个正则表达式。

这是我现在使用的代码:

// convert raw input to valid XML representation
$data = str_replace(
    array('<', '>', '&'), 
    array('&lt;', '&gt;', '&amp;'), 
    $data
);
$data = preg_replace(
    '!begin-(command|othercommand|morecommand)(?:-(?P<options>\S+))?!', 
    '<\1 options="\2">', 
    $data
);
$data = preg_replace(
    '!(command|othercommand|morecommand)-end!', 
    '</\1>', 
    $data
);

// use DOM to parse XML representation
$dom = new \DOMDocument();  
$dom->loadXML("<?xml version='1.0' ?>\n<document>".$data.'</document>');
$xpath = new \DOMXPath($dom);

// iterate over DOM, recursively replace commands with conversion results
foreach($xpath->query('./*') as $node) {
    if ($node->nodeType == XML_ELEMENT_NODE)
        convertNode($node, 'form', $dom, $xpath);
}

// convert XML DOM back to raw format
$data = $dom->saveXML();
$data = substr($data, strpos($data, "<document>")+10, -12);
$data = str_replace(
    array('&amp;', '&lt;', '&gt;'), 
    array('&', '<', '>'), 
    $data
);

// output the stuff
echo $data;

function convertNode (\DomNode $node, $output_mode, $dom, $xpath) {
    $type = $node->tagName;
    $children = $xpath->query('./*', $node);

    // recurse over child nodes
    foreach ($children as $childNode) {
        if ($childNode->nodeType == XML_ELEMENT_NODE) {
            convertNode($childNode, $output_mode, $dom, $xpath);
        }
    }

    // in production code, here is actual logic
    // to process the several command types
    $newNode = $dom->createTextNode(
        "<$type>" 
        . $node->textContent
        . "</$type>"
    );

    // replace node with command result
    if ($node->parentNode) {
        $node->parentNode->replaceChild($newNode, $node);
        // just to be sure - normalize parent node
        $newNode->parentNode->normalize();
    } 
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-05
    • 2015-08-03
    相关资源
    最近更新 更多