PHP中的DOM:解码实体和设置nodeValue

时间:2013-06-26 13:43:22

标签: php dom encoding libxml2

我想使用PHP标准库的DOM部分对PHP文档执行某些操作。由于其他人已经discovered,因此必须处理已解码的实体。为了说明困扰我的是什么,我举了一个简单的例子。

假设我们有以下代码

$doc = new DOMDocument();
$doc->loadXML(<XML data>);

$xpath = new DOMXPath($doc);
$node_list = $xpath->query(<some XPath>);

foreach($node_list as $node) {
    //do something
}

如果循环中的代码类似于

$attr = "<some string>";
$val = $node->getAttribute($attr);
//do something with $val
$node->setAttribute($attr, $val);

它工作正常。但如果它更像是

$text = $node->textContent;
//do something with $text
$node->nodeValue = $text;

$text包含一些已解码的&amp; ,即使根本不对$text执行任何操作,也不会对其进行编码。

目前,我在$text之前$node->nodeValue申请htmlspecialchars。现在我想知道

  1. 如果这就足够了,
  2. 如果没有,那就足够了,
  3. 如果有更优雅的解决方案,就像属性操作一样。
  4. 我必须处理的XML文档主要是提要,因此解决方案应该非常通用。


    修改

    原来我原来的问题范围不对,对不起。在这里,我提供了一个实际描述行为的例子。

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, "http://feeds.bbci.co.uk/news/rss.xml?edition=uk");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $output = curl_exec($ch);
    curl_close($ch);
    
    $doc = new DOMDocument();
    $doc->loadXML($output);
    
    $xpath = new DOMXPath($doc);
    $node_list = $xpath->query('//item/link');
    
    foreach($node_list as $node) {
            $node->nodeValue = $node->textContent;
    }
    echo $doc->saveXML();
    

    如果我使用

    在CLI上执行此代码
    php beeb.php |egrep 'link|Warning'
    

    我得到的结果如

      

    &LT; LINK&GT; http://www.bbc.co.uk/news/world-africa-23070006#sa-ns_mchannel=rss</link&GT;

    应该是

      

    &LT; LINK&GT; http://www.bbc.co.uk/news/world-africa-23070006#sa-ns_mchannel=rss&ns_source=PublicRSS20-sa</link&GT;

    (并且,如果省略循环)并根据警告

      

    警告:第15行的/private/tmp/beeb.php中的main():未终止的实体引用ns_source = PublicRSS20-sa

    当我将htmlspecialchars应用于$node->textContent时,它运行正常,但我觉得这样做非常不舒服。

2 个答案:

答案 0 :(得分:6)

您的问题基本上是将DOMText::nodeValue设置为XML编码字符串还是逐字字符串。

所以,让我们试试看,然后将其设置为&'&amp;,看看会发生什么:

$doc = new DOMDocument();
$doc->loadXML('<root>*</root>');

$text = $doc->documentElement->childNodes->item(0);

echo "Before Edit: ", $doc->saveXML($text), "\n";

$text->nodeValue = "&";

echo "After Edit 1: ", $doc->saveXML($text), "\n";

$text->nodeValue = "&amp;";

echo "After Edit 2: ", $doc->saveXML($text), "\n";

然后输出如下(PHP 5.0.0 - 5.5.0):

Before Edit: *
After Edit 1: &amp;
After Edit 2: &amp;amp;

这表明设置nodeValue - DOMText节点需要UTF-8编码字符串,DOM库会自动编码XML保留字符。

所以你应该htmlspecialchars()应用到你用这种方式添加的任何文本上。这将创建一个双重编码。

在您编写的时候,您会遇到相反的情况我建议您在命令行/ IDE中执行一个独立的PHP示例,以便您可以准确地看到输出。并不是说您的浏览器将其呈现为HTML,然后您认为保留的XML字符尚未编码。


正如您所指出的那样,您不是在编辑DOMText而是编辑DOMElement节点。它有点不同,这里&字符需要作为实体&amp;而不是逐字传递,但只有这个字符。

所以这需要更多的工作:

  1. 读出文本内容并将其转换为DOMText节点。一切都将完美编码。
  2. 删除元素节点的节点值,使其为空。
  3. DOMText节点表单作为子级添加到第一步。
  4. 完成了。在这里你的内在foreach修改显示:

    foreach($node_list as $node) {
        $text = $doc->createTextNode($node->textContent);
        $node->nodeValue = "";
        $node->appendChild($text);
    }
    

    对于你的具体例子,虽然我必须承认我不明白为什么你这样做,因为这不会改变价值所以它不需要这个。

      

    提示: 在PHP中,DOMDocument可以直接打开此Feed,这里不需要卷曲:

    $doc = new DOMDocument();
    $doc->load("http://feeds.bbci.co.uk/news/rss.xml?edition=uk");
    

答案 1 :(得分:2)

正如hakre所解释的,问题是在PHP的DOM库中,设置 nodeValue w.r.t的行为。实体取决于节点的类别,特别是DOMTextDOMElement在这方面有所不同。 为了说明这一点,举个例子:

$doc = new DOMDocument();
$doc->formatOutput = True;
$doc->loadXML('<root/>');

$s = 'text &amp;&lt;<"\'&text;&text';

$root = $doc->documentElement;

$node = $doc->createElement('tag1', $s); #line 10
$root->appendChild($node);

$node = $doc->createElement('tag2');
$text = $doc->createTextNode($s);
$node->appendChild($text);
$root->appendChild($node);

$node = $doc->createElement('tag3');
$text = $doc->createCDATASection($s);
$node->appendChild($text);
$root->appendChild($node);

echo $doc->saveXML();

输出

Warning: DOMDocument::createElement(): unterminated entity reference            text in /tmp/DOMtest.php on line 10
<?xml version="1.0"?>
<root>
  <tag1>text &amp;&lt;&lt;"'&text;</tag1>
  <tag2>text &amp;amp;&amp;lt;&lt;"'&amp;text;&amp;text</tag2>
  <tag3><![CDATA[text &amp;&lt;<"'&text;&text]]></tag3>
</root>

在这种特殊情况下,更改DOMText个节点的 nodeValue 是合适的。结合hakre's两个答案,可以得到一个非常优雅的解决方案。

$doc = new DOMDocument();
$doc->loadXML(<XML data>);

$xpath     = new DOMXPath($doc);
$node_list = $xpath->query(<some XPath>);

$visitTextNode = function (DOMText $node) {
    $text = $node->textContent;
    /*
        do something with $text
    */
   $node->nodeValue = $text;
};

foreach ($node_list as $node) {
    if ($node->nodeType == XML_TEXT_NODE) {
        $visitTextNode($node);
    } else {
        foreach ($node->childNodes as $child) {
            if ($child->nodeType == XML_TEXT_NODE) {
                $visitTextNode($child);
            }
        }
    }
}