简单XML,请不要扩展实体


SimpleXML, please do not expand entities

我正在使用SimpleXML来尝试解析带有<!ENTITY声明的大型XML文件。不幸的是,SimpleXML 似乎太急于继续扩展这些实体,我宁愿它没有,因为实体符号很短,易于解析,理论上不会在较新版本的文件中更改,而扩展的实体是可能会更改的英语句子。有没有办法告诉SimpleXML把它敲掉?

在将文件内容传递给 XML 解析器之前,我想过"预解析"XML 文件以去除<!ENTITY位,但这感觉很黑客,而且由于它是一个大文件,我宁愿尽可能少地摆弄它。

(请原谅上述任何错误的术语;我已经很久没有做过这种级别的 XML 工作了。

看起来是这样,但事实并非如此(除非您指定我猜您没有的标志,尽管您没有在代码中显示您的工作)。只是 SimpleXML 只有在您使用 ->asXML() 方法而不是通过 to-string 实现时才能将其返回给您。

让我们做一些例子来演示它是如何工作的。我从 DTD 中选择了这个简单的实体:

<!ENTITY n "noun (common) (futsuumeishi)">

因此,让我们选择第一个<pos>元素,因为它包含一个&n;实体:

$xml = simplexml_load_file($file);
$pos = $xml->entry->sense->pos;

变量$pos现在是<pos>元素节点的 SimpleXMLElement。让我们输出它以查看解析器如何处理&n;实体:

echo  "SimpleXML value (string): ", $pos         , "'n"
    , "SimpleXML value (XML)   : ", $pos->asXML(), "'n";

输出为:

SimpleXML value (string): noun (common) (futsuumeishi)
SimpleXML value (XML)   : <pos>&n;</pos>

如本例所示,&n;仍然存在(<pos>&n;</pos>),只是在您作为字符串值(noun (common) (futsuumeishi))访问它的那一刻,它将被扩展。

顺便说一下,这是完全可以的,XML 规范在这里说,是否扩展这些实体取决于解析器。对于 SimpleXML 的设计目的,在读取字符串值时完全可以扩展。

您甚至可以通过指定LIBXML_NOENT选项来控制此行为:

$xml = simplexml_load_file($file, NULL, LIBXML_NOENT);

这实际上将执行您当时假设的操作,实体现在已展开,XML 输出不再包含该实体:

SimpleXML value (string): noun (common) (futsuumeishi)
SimpleXML value (XML)   : <pos>noun (common) (futsuumeishi)</pos>

所以现在双问号如何做你想要的?好吧,PHP中实际上具有实体模型的XML解析器是DOMDocument。它是SimpleXML的姊妹库,在内部共享相同的内存对象。以下是同一对象(更准确地说:它唯一的子节点)对于这两种模式的输出,不带LIBXML_NOENT

Mode 1:
DOMDocument Class       : DOMEntityReference
DOMDocument value(XML)  : &n;
DOMDocument ->nodeName  : n
Mode 2 (LIBXML_NOENT):
DOMDocument Class       : DOMText
DOMDocument value(XML)  : noun (common) (futsuumeishi)
DOMDocument ->nodeName  : #text

这是由以下代码创建的,该代码应该使给定输出背后的内容更加可见:

$node   = dom_import_simplexml($pos);
$doc    = $node->ownerDocument;
$entity = $node->firstChild;
echo  "DOMDocument Class       : ", get_class($entity)    , "'n"
    , "DOMDocument value(XML)  : ", $doc->saveXML($entity), "'n"
    , "DOMDocument ->nodeName  : ", $entity->nodeName     , "'n";

正如所写,它是一个姊妹库,dom_import_simplexml $pos变成了一个DOMElement,我们需要遍历它的子库,我们知道这是有问题的实体引用。

所以现在这开始变得非常有意义:由于 SimpleXML 不能表示实体引用,它只能提供扩展的字符串值包含实体的 XML。

否则有什么方法可以改变字符串值

<pos>&n;</pos>
<pos><![CDATA[&n;]]></pos>

?所以你所要求的只是有限的意义。然而,这并不意味着我们不能处理这个问题,因此可以通过从它扩展来欺骗SimpleXML来做到这一点。假设每个只包含一个实体的子元素都应该返回 so。否则,应使用标准的 SimpleXML 字符串化:

/**
 * Class EntityPreserveXML
 */
class EntityPreserveXML extends SimpleXMLElement
{
    /**
     * @return string
     */
    public function __toString()
    {
        $dom = dom_import_simplexml($this);
        if (
            !$dom instanceof DOMElement
            || $dom->childNodes->length !== 1
            || ! $dom->firstChild instanceof DOMEntityReference
        ) {
            return parent::__toString();
        }
        return $dom->ownerDocument->saveXML($dom->firstChild);
    }
}

让我们让它在上面的示例中运行:

require('EntityPreserveXML.php');
$xml = simplexml_load_file($file, 'EntityPreserveXML');
$pos = $xml->entry->sense->pos;
echo  "SimpleXML value (string): ", $pos         , "'n"
    , "SimpleXML value (XML)   : ", $pos->asXML(), "'n";

SimpleXML 现在正在使用扩展类,然后按预期给出:

SimpleXML value (string): &n;
SimpleXML value (XML)   : <pos>&n;</pos>

&n;因为它是唯一的子项,现在保留在 SimpleXMLElement 的字符串转换中。但仅仅因为这工作并不意味着你应该使用它,它打破了文本形式的解析 XML 和文档模型意义上的 XML 之间的编码边界。

可能你只是在寻找DOMDocument?这是一个具有更多细节的模型,如果有的话,您可以从中使用DOMEntityReference