【问题标题】:How to check if a DomDocument was changed with a simple and fast comparison?如何通过简单快速的比较来检查 DomDocument 是否已更改?
【发布时间】:2014-07-05 12:51:22
【问题描述】:

我只需要在某些操作之前和之后比较相同的DomDocument 对象,“快速检查”操作是否更改了对象...我需要一些serialize_DomDocument()存在类似的东西吗?) 可以在以下上下文中使用:

  1. $obj 是一个DomDocumemt 对象(“对象的状态”是所有属性和所有 documentElement 内容的值)。

  2. $dump = serialize_DomDocument($obj)   怎么做?? 如何转储对象的状态? ... 不是通过 saveXML() 方法将所有内容转换为 XML,而只是(更快!)将所有二进制表示($obj 指向的对象)处理为 $dump

  3. 执行一些操作(例如删除节点或更改属性或“什么都不做”)

  4. if ($dump != serialize_DomDocument($obj)) 或类似的东西,检查$obj 是否改变。快速比较。 操作是“什么都不做”还是“做某事”?

替代解决方案...

不是理想的,但可以解决一些情况...有一些操作(例如,只有appendChild 或只有removeChild),其中的更改总是会影响节点的数量,所以,对于那种检查节点总数总长度的操作就足够了。

如何比saveXML()检查更快?


注意事项

注意-1:正如 this post 所记得的,您不能序列化 DomDocument 对象。

NOTE-2:不是comparing two distinct DOM objects的问题,而是更简单的事情,因为不改变ID等。不需要规范表示(!),只访问内部表示。


赏金后编辑

这个问题不是关于“控制如何改变或改变标志”,请仔细阅读这个问题。

这个问题不是理论评论的要求

编辑2

也许解决方案是关于“低级”...我不明白“二进制表示”是“转储”还是有其他名称,请参阅:

【问题讨论】:

标签: php domdocument


【解决方案1】:

这里是“二等”解决方案,hoping "first class" here

...在没有直接解决方案的情况下,有“替代解决方案”,正如我在问题文本中所表达的...这就是我今天在我的应用程序中所做的事情:

使用 XPath count(//node()) 我们可以检查是否更改,比 saveXML() 更快,但如果 DOM 具有相同数量的节点(例如更改属性值),我们需要通过 saveXML 进行检查,花费内存和/或 CPU 时间来生成和比较 XML。

// $old_dom is the original document, 
$old_xp = new DOMXpath($old_dom);
// ... holding $old_n (and perhaps $old_xml) in a cache 
$old_n = $old_xp->evaluate('count(//node())');
$old_xml = $old_dom->saveXML();
...

// $new_dom is the modified document, must tested at each "black box" change.
$new_xp = new DOMXpath($new_dom);
$new_n = $new_xp->evaluate('count(//node())');
if ($new_n!=$old_n) {  //  OK, fast!!
  //.. OK! do something because changed!
} // else, check with more detail if changed,
elseif ($new_dom->saveXML()!=$old_xml){ // memory and CPU-time consuming here!
  //.. OK! do something because changed!
}

正如@ArtisiticPhoenix 建议的那样,让我们​​比较一下!

基准测试this simple gistreal life XML-documents of PubMed Central

  $xp = new DOMXpath($dom);      
  $test = ( $xp->evaluate('count(//node())') == 123 );
    // Execution total time of 10000 loops: 0.582 seconds
    // Averaged execution time of each loop: 5.8152985572815E-5 seconds
  
  $xp = new DOMXpath($dom);      
  $test = ( $xp->evaluate('count(//*)') == 123 );
    // Execution total time of 10000 loops: 0.462 seconds
    // Averaged execution time of each loop: 4.6241903305054E-5 seconds

与其他解决方案相比,要好 10 倍,

  $test = ( md5( $dom->saveXML() ) == 'ff1afedc7127eb221050fa48eee5153a');
    // Execution total time of 10000 loops: 3.877 seconds
    // Averaged execution time of each loop: 0.00038769061565399 seconds

  $test = ( $dom->saveXML() == $oldXml ); // comparing long strings 
    // Execution total time of 10000 loops: 3.168 seconds
    // Averaged execution time of each loop: 0.00031677310466766 seconds

  $test = ( $dom->C14N() == $oldXml ); // comparing long strings 
    // Execution total time of 10000 loops: 8.874 seconds
    // Averaged execution time of each loop: 0.00088736770153046 seconds

注意:直接 longString 比较shortString by md5 快。确认,

  $test = ( $dom->saveXML() == 'xxx' );
    //Execution total time of 10000 loops: 3.14 seconds
    //Averaged execution time of each loop: 0.00031396999359131 seconds

长/短的变化,即使在最坏的情况下(比较相同的字符串),也不会改变“0.31微秒”来直接执行,优于short+MD5的“0.39微秒”。


我在这种“替代解决方案”中寻找什么......

... 是一个 DomDocument 内部属性,它返回“DOM 长度”或“DOM 节点数”,无需 XPath 计数过程。

PS:“替代解决方案”的赏金就是这种答案。

【讨论】:

    【解决方案2】:

    首先我没有使用过 DOMDocument,但我会试一试(请阅读完整的帖子)。

    您可以使用 C14N() 方法,它没有很好的文档记录,但是从我的 IDE 中我得到了这个:

    /**
     * Canonicalize nodes to a string
     * @link http://www.php.net/manual/en/domnode.c14n.php
     * @param exclusive bool[optional] <p>
     * Enable exclusive parsing of only the nodes matched by the provided
     * xpath or namespace prefixes.
     * </p>
     * @param with_comments bool[optional] <p>
     * Retain comments in output.
     * </p>
     * @param xpath array[optional] <p>
     * An array of xpaths to filter the nodes by.
     * </p>
     * @param ns_prefixes array[optional] <p>
     * An array of namespace prefixes to filter the nodes by.
     * </p>
     * @return string canonicalized nodes as a string&return.falseforfailure;
    */
    public function C14N ($exclusive = null, $with_comments = null, array $xpath = null, array $ns_prefixes = null) {}
    

    我将简单地从这篇文章的 PHP 文档页面中取出 DOMDocument 的示例。

    所以对于我的例子来说,我有这个 obj 开始。 (请注意我将 for 循环放在 cmets 中的位置,我将使用后者进行基准测试):

        $xml = new \DOMDocument( "1.0", "ISO-8859-15" );
        $xml_album = $xml->createElement( "Album" );
        //---- for( $i=0; $i < 10000; $i++ ){  //for benchmarks I will be adding 30,000 nodes, to get something worth measuring performance on.
        // Create some elements.
        $xml_track = $xml->createElement( "Track", "The ninth symphony" );
    
        // Set the attributes.
        $xml_track->setAttribute( "length", "0:01:15" );
        $xml_track->setAttribute( "bitrate", "64kb/s" );
        $xml_track->setAttribute( "channels", "2" );
    
        // Create another element, just to show you can add any (realistic to computer) number of sublevels.
        $xml_note = $xml->createElement( "Note", "The last symphony composed by Ludwig van Beethoven." );
    
        // Append the whole bunch.
        $xml_track->appendChild( $xml_note );
        $xml_album->appendChild( $xml_track );
    
        // Repeat the above with some different values..
        $xml_track = $xml->createElement( "Track", "Highway Blues" );
    
        $xml_track->setAttribute( "length", "0:01:33" );
        $xml_track->setAttribute( "bitrate", "64kb/s" );
        $xml_track->setAttribute( "channels", "2" );
        $xml_album->appendChild( $xml_track );
    
        $xml->appendChild( $xml_album );
        //----- } //end for loop
        // Parse the XML.
        print $xml->saveXML();
    

    或者大致是当我们使用 htmlspecialchars 和一些制表符对其进行编码时:

    <?xml version="1.0" encoding="ISO-8859-15"?>
    <Album>
        <Track length="0:01:15" bitrate="64kb/s" channels="2">The ninth symphony
            <Note>The last symphony composed by Ludwig van Beethoven.</Note>
        </Track>
        <Track length="0:01:33" bitrate="64kb/s" channels="2">Highway Blues</Track>
    </Album>
    

    到目前为止很好,现在使用(文档记录不佳的 C14N() )给了我们这个(减去漂亮的缩进等),注意它们几乎相同,但顺序不同,我们减去了编码位,所以我们不想将它们相互比较:

    <Album>
        <Track bitrate="64kb/s" channels="2" length="0:01:15">The ninth symphony
            <Note>The last symphony composed by Ludwig van Beethoven.</Note>
        </Track>
        <Track bitrate="64kb/s" channels="2" length="0:01:33">Highway Blues</Track>
    </Album>
    

    现在通常这似乎与 saveXML 类似,但它有更多用于过滤输出的选项,而不仅仅是 saveXML,所以我想我会提到它。

    现在我不完全确定为什么在有限的测试中对性能的关注我冒昧地为 30,000 个节点(20,000 个轨道、10,000 个音符节点和 60,000 个属性)循环了 10,000 次,即使这样,性能也相当很高兴给我这些结果(仅针对下面显示的函数调用,不生成 DOM 内容,因为这是一个单独的问题):

     $xml->saveXML();
    'elapsedTime' => '0.10 seconds',
    'elapsedMemory' => '0.39 KB'
    
     $xml->C14N();
    'elapsedTime' => '0.15 seconds',
    'elapsedMemory' => '0.3 KB'
    
      /// outputting to the screen should not be tracked - as I show below this will have a slight, but non-zero impact on the performance benchmarks.
      echo $xml->saveXML()
     'elapsedTime' => '0.16 seconds', //+0.06 seconds
     'elapsedMemory' => '0.3 KB'
    
      echo $xml->C14N();
     'elapsedTime' => '0.21 seconds', //+0.06 seconds again
     'elapsedMemory' => '0.3 KB'
    

    所以性能略低于 saveXML,但在这两种情况下,我会说我使用的节点数量是非常合理的。

    既然我们可以接受使用 saveXML 或 C14N,我们如何将更改与如此大的字符串进行比较?我们每个人都应该知道,你散列它。现在人们会立即想到 md5,但实际上这里的 sha1 更好,它给我们的哈希值稍长,性能差异可以忽略不计。在这两种情况下,散列增加了大约百分之一秒,并让我们在比较、保存在数据库等时更容易查看。

    -- 作为旁注,我喜欢散列,它就像环氧树脂胶水或胶带,它适用于所有东西。

    所以我们只需对其进行哈希处理,将其保存到一个变量中并进行比较:

     print md5( $xml->saveXML() );
    
    '19edc177072416b7bbf88ea0a240be73'
    'elapsedTime' => '0.11 seconds',
    'elapsedMemory' => '0.39 KB'
    
    
     print sha1( $xml->saveXML() );
    
    '7c644c6e1630ffde15eee64643779e415a1746b7'
    'elapsedTime' => '0.11 seconds',
    'elapsedMemory' => '0.3 KB'
    

    现在我可能会因为使用 saveXML() (和/或 C14N() )而受到打击,但最终归结为这个。甚至计算可以通过这种方式完成的属性(只是为了涵盖我的基础):

     $old_xp = new \DOMXpath($xml);
     $old_a = $old_xp->evaluate('count( //@* )');
     $old_n = $old_xp->evaluate('count( //node() )');
     print 'Attributes: '.$old_a.'<br>';
     print 'Nodes: '.$old_n.'<br>';
     print 'Total: '.($old_a + $old_n).'<br>';
    

    输出:/1 次迭代(检查上面发布的 xml):

     Attributes: 6
     Nodes: 7  //expected 4 nodes
     Total: 13
    

    输出:/ 10,000 次迭代:

    'elapsedTime' => '0.02 seconds',
    'elapsedMemory' => '0.5 KB'
     Attributes: 60000
     Nodes: 60001  //expected 30,001 nodes ( +2 tracks, +1 note, node per loop and one album node )?
     Total: 120001
    

    如您所见,时间更快,但是因为我们在这里实例化 DOMXpath,如果您已经有一个可用的实例,这可能不会影响您,内存消耗几乎翻了一番。

    -- 作为旁注,$old_xp-&gt;evaluate('count( //node() )') 似乎为我期望 4 个节点并得到 7 个节点的节点提供了奇怪的计数,就像它计算打开和关闭标签以及编码标签,或者计算每个节点的嵌套子节点(通过在第二条轨道上添加一个音符节点来检查这一点,它没有,并且计数确实增加了 2 )关于这方面的任何更多信息都会有所帮助。

    反正你知道这个方法的其余部分。

    但是,在使用计数时,如果您要删除 1 个属性并添加另一个属性,则会错误地计算属性,这同样适用于节点(但计数很奇怪)。

    但是,最终没有办法知道它是否改变了,如果不查看实际数据,如果一个节点的内容发生了变化怎么办?等等……

    这(只是数数)可能足以满足您的需求

    选择权在您手中,这实际上取决于您需要什么级别的详细信息,以及您愿意为该级别的详细信息承担多少性能损失。

    我建议对每个步骤进行彻底的基准测试,然后确定哪个更能满足您的需求。

    最后只是生成 XML 给了我以下时间/内存使用情况(记住保存和散列只有 0.11 秒):

    'elapsedTime' => '21.16 seconds',
    'elapsedMemory' => '0.61 KB'
    

    我们在这里谈论性能,但没有给出数字,因此当我们根据性能做出决策时,我们确实需要将事情放在上下文中。

    谢谢,

    【讨论】:

    • 我看到你的方法是正确的:编码和基准测试(!)。关于 C14N,请参阅我在 NOTE-2 中写的“...不需要规范表示 (!)...”,以及我如何分类黑盒 in this other post。关于 C14N 的性能,见the explosion with big node-numbers
    • 关于您在“Alternative solution”上下文中的良好性能分析和建议:从节点数更改为sha1,这是一个好问题,谢谢!我现在必须测试并做功课,以验证您的建议。
    • @关于 C14N 的性能 - 嵌套越深我最多只能嵌套 3 层,我可以看到它如何以指数方式增加节点树的深度。
    • Ops,关于您对$old_a + $old_n 的使用,XPath 将属性和元素视为节点(!),总结起来没有意义。 DOM 模型将属性集合(不是孤立的属性)视为“树中的项目”的唯一时刻是在替换操作中,请参阅this DOM-tree modeling consideration
    • 由于您在正确道路上的努力,我被选为您的答案;但您没有得到赏金,因为您的结果似乎错误。我今天做功课,结果 (see here) 是 XPath 好约 10 倍,字符串直接比较比 MD5 快约 25%。 PS:要获得 50% 的赏金,您需要更多 1 票。
    【解决方案3】:

    这个一般问题有三种可能的解决方案:

    1) 按照您的建议保存副本(或序列化副本)。此解决方案可以完全在外部完成(假设存在克隆或序列化)。但是,正如您所提到的,对于复杂的对象,它可能会非常慢。

    2) 提供服务(变更检测)作为 DomDocument(或 DomNode)类的功能。此解决方案要求您有权访问 DomDocument 的定义,因此您可能不想考虑它。但是,为了提高效率,这是放置服务的“正确”位置,因为类本身最清楚如何确定任何操作是否会在 DOM 中产生变化。

    3) 提供服务(更改记录)作为上述步骤 3. 的一项功能。那就是让代码“执行一些操作”来跟踪是否进行了更改。对于某些操作,这可能需要保留 DOM 的一小部分本地化部分的本地副本(或序列化副本)。或者,它可能需要在执行操作之前检查某些节点的当前状态,以确定是否发生了更改。但是,对于大多数操作,答案应该是相当明显的。

    【讨论】:

    • 感谢您的努力,但是,请不要只是理论上的答案...我需要一个实用的 PHP 解决方案,这里有代码。
    • 你知道二进制表示函数吗,libxml-tree dumps
    • 如果您有新问题,请向整个社区提问。
    • 是的,我在“EDIT2”之前用过,见那里。
    【解决方案4】:

    嗯,我看了DOMDocument的Object结构。看起来没有简单的方法可以做到这一点。我的第一个解决方案是比较 saveXML 方法的结果。但这不是你想要的。让我们让它更快:

    变体 A:

    使用内部 serialize php 函数:http://php.net/manual/de/function.serialize.php。这也将比较整个文档,但以序列化形式。可能比比较 saveXML 结果快一点。

    变体 B:

    saveXML 方法的第一个参数很有趣。如果您知道哪些节点可能已更改,您可以将输出减少到只有这些(请参阅:http://php.net/manual/de/domdocument.savexml.php)。这样您就不必比较整个文档。

    变体 C:

    如果 DOMDocument 对象无法自行确定更改,让我们教它这样做!这看起来像这样(尚未对其语法进行测试,但重要的是这个想法):

    use DOMDocument;
    use DOMNode;
    
    class AlterationSensitiveDOMDocument extends DOMDocument
    {
        /**
         * @var bool
         *   Determines whether the DOMDocument was altered or not.
         */
        protected $isAltered = false;
    
        /**
         * @param DOMNode $newnode
         *
         * @return DOMNode|void
         */
        public function appendChild(DOMNode $newnode)
        {
            $this->isAltered = true;
    
            return parent::appendChild($newnode);
        }
    
        // More overrides (The ones you need)
    
        /**
         * @return boolean
         */
        public function isAltered()
        {
            return $this->isAltered;
        }
    }
    

    通过使用新的 AlterationSensitiveDOMDocument 而不是 DOMDocument,您将可以完全访问 DOMDocument 参数和方法。加上一些更改方法(您覆盖的方法)会将状态“isAltered”设置为 true,因此您可以查看 DOMDocument 对象是否被更改。

    这个解决方案闻起来像 hack,但应该可以解决问题。

    【讨论】:

    • 抱歉,请仔细阅读,我已经注意到所有...关于您的 Variant-B)我说 不是通过 saveXML() 方法将所有内容都转换为 XML,而只是(更快!)处理所有二进制表示;关于你的 Variant-C) 看我的新强化 这个问题不是关于“控制如何改变或改变标志”
    • 你知道二进制表示函数吗,libxml-tree dumps? (见 EDIT2)。
    • 再次读取变体 B。我在哪里说过您必须将所有内容都转换为 XML?
    • 我的问题是关于“整个文档”,检查是否有任何字节改变......正如我所说,唯一的“替代解决方案”(不太好)是检查节点数,我今天正在做什么:使用 XPath count(//node()) 我可以检查更改是否比saveXML() 更快,但是如果 DOM 具有相同数量的节点(例如更改属性值),我需要检查 saveXML,花费 CPU 时间生成和比较 DOM。
    • PS:很抱歉您在AlterationSensitiveDOMDocument 课程上的努力,我有一个“黑匣子问题”(例如,执行 XSLT 我不能说是否更改了),所以我在问题中说像你的变体-C 这样的东西是无效的。
    猜你喜欢
    • 1970-01-01
    • 2011-12-03
    • 1970-01-01
    • 2018-12-21
    • 1970-01-01
    • 1970-01-01
    • 2015-07-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多