递归地解析自定义标记

时间:2014-01-03 12:30:40

标签: php parsing recursion

我必须处理一个已经存在的自定义标记语言(这很难看,但遗憾的是无法更改因为我正在处理旧数据并且它需要与旧应用程序保持兼容)。

我需要解析命令“范围”,并根据用户采取的操作将数据中的这些“范围”替换为其他内容(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;

2 个答案:

答案 0 :(得分:0)

你在你的正则表达式的第4行展望未来:

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

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

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

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

如果(?: - if)?在开始命令之后总是会来的,你可以在那里摆脱它,会使它像:

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

答案 1 :(得分:0)

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

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

  1. 使用文本表示(使用XML实体)替换可以解释为XML的所有内容
  2. 将所有begin-command ... command-end块替换为相应的XML标记
    (注意实际上有几个不同的命令)
  3. 让真正的解析器(XML DOM)处理标记树
  4. 递归迭代DOM
  5. 对于每个节点,根据操作模式执行相应的操作
  6. 这看起来很难看,但我真的不想写自己的解析器 - 这似乎有点过分&#34;在有限的时间内,我有提高速度。哦,男孩,这仍然是快速的 - 比RegExp解决方案快得多。当你考虑将原始输入转换为有效的XML并返回时,令人印象深刻。

    &#34;快速开始&#34;我的意思是,对于一个以前需要5-7秒来解析几个正则表达式的文档,它现在需要大约200ms。

    以下是我现在使用的代码:

    // 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();
        } 
    }