无论结构或名称空间如何,都使用SimpleXML解析名称空间

时间:2014-10-16 09:35:21

标签: php xml simplexml xml-namespaces

我收到了这样的Google购物Feed(摘录):

<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">  
...
<g:id><![CDATA[Blah]]></g:id>
<title><![CDATA[Blah]]></title>
<description><![CDATA[Blah]]></description>
<g:product_type><![CDATA[Blah]]></g:product_type>

现在,SimpleXML可以读取&#34;标题&#34;和&#34;描述&#34;标签,但它不能用&#34; g:&#34;前缀。

针对此特定情况,stackoverflow上有解决方案,使用&#34; children&#34;功能。 但我不想只阅读Google购物XML,我需要它不受结构或命名空间的影响,我不知道有关该文件的任何信息(我以递归方式循环遍历节点作为多维数组)。

有没有办法用SimpleXML做到这一点?我可以替换冒号,但我希望能够存储数组并重新组合XML(在这种情况下专门用于Google购物),所以我不想丢失信息。

2 个答案:

答案 0 :(得分:9)

您希望使用 SimpleXMLElement 从XML中提取数据并将其转换为数组。

这通常是可能的,但有一些警告。在XML命名空间之前,您的XML附带CDATA。对于使用Simplexml进行XML到数组转换,您需要在加载XML字符串时将CDATA转换为文本。这是通过LIBXML_NOCDATA标志完成的。例如:

$xml = simplexml_load_string($buffer, null, LIBXML_NOCDATA);
print_r($xml); // print_r shows how SimpleXMLElement does array conversion

这为您提供以下输出:

SimpleXMLElement Object
(
    [@attributes] => Array
        (
            [version] => 2.0
        )

    [title] => Blah
    [description] => Blah
)

正如您已经看到的,没有很好的表单可以在数组中显示属性,因此按惯例,Simplexml会将这些属性放入@attributes键。

您遇到的另一个问题是处理这些多个XML命名空间。在前面的示例中,未使用特定的命名空间。那是默认名称空间。将 SimpleXMLElement 转换为数组时,会使用 SimpleXMLElement 的命名空间。由于没有明确指定,因此已采用默认命名空间。

但是如果在创建数组时指定命名空间,则 命名空间。

示例:

$xml = simplexml_load_string($buffer, null, LIBXML_NOCDATA, "http://base.google.com/ns/1.0");
print_r($xml);

这为您提供以下输出:

SimpleXMLElement Object
(
    [id] => Blah
    [product_type] => Blah
)

正如您所看到的,这次创建 SimpleXMLElement 时指定的命名空间用于数组转换:http://base.google.com/ns/1.0

在您编写时,您希望将文档中的所有命名空间考虑在内,您需要先获取这些命名空间 - 包括默认命名空间:

$xml = simplexml_load_string($buffer, null, LIBXML_NOCDATA);
$namespaces = [null] + $xml->getDocNamespaces(true);

然后,您可以遍历所有名称空间和recursively merge them into the same array,如下所示:

$array = [];
foreach ($namespaces as $namespace) {
    $xml = simplexml_load_string($buffer, null, LIBXML_NOCDATA, $namespace);
    $array = array_merge_recursive($array, (array) $xml);
}
print_r($array);

然后最终应该创建并输出您选择的数组:

Array
(
    [@attributes] => Array
        (
            [version] => 2.0
        )

    [title] => Blah
    [description] => Blah
    [id] => Blah
    [product_type] => Blah
)

正如您所看到的, SimpleXMLElement 完全可以实现。但是,了解 SimpleXMLElement 如何转换为数组(或序列化为遵循相同规则的JSON)非常重要。要模拟 SimpleXMLElement 到数组转换,您可以使用print_r来快速输出。

请注意,并非所有XML构造都可以很好地转换为数组。这并不是Simplexml的特别限制,而在于XML可以表示的结构的性质以及数组可以表示的结构。

因此,最好将XML保存在像 SimpleXMLElement (或 DOMDocument )这样的对象中,以访问和处理数据 - 而不是使用数组。

然而,只要您知道自己做了什么并且不需要编写太多代码来访问结构中树下更深层的成员,将数据转换为数组就完全没问题了。否则 SimpleXMLElement 比数组更受青睐,因为它不仅允许对许多XML功能进行专用访问,而且还可以像使用SimpleXMLElement::xpath method的数据库一样进行查询。您需要编写许多自己的代码行来访问XML树中适合数组的数据。

为了充分利用这两个方面,您可以扩展 SimpleXMLElement 以满足您的特定转换需求:

$buffer = <<<BUFFER
<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
...
<g:id><![CDATA[Blah]]></g:id>
<title><![CDATA[Blah]]></title>
<description><![CDATA[Blah]]></description>
<g:product_type><![CDATA[Blah]]></g:product_type>
</rss>
BUFFER;

$feed = new Feed($buffer, LIBXML_NOCDATA);
print_r($feed->toArray());

输出:

Array
(
    [@attributes] => stdClass Object
        (
            [version] => 2.0
        )

    [title] => Blah
    [description] => Blah
    [id] => Blah
    [product_type] => Blah
    [@text] => ...
)

对于底层实现:

class Feed extends SimpleXMLElement implements JsonSerializable
{
    public function jsonSerialize()
    {
        $array = array();

        // json encode attributes if any.
        if ($attributes = $this->attributes()) {
            $array['@attributes'] = iterator_to_array($attributes);
        }

        $namespaces = [null] + $this->getDocNamespaces(true);
        // json encode child elements if any. group on duplicate names as an array.
        foreach ($namespaces as $namespace) {
            foreach ($this->children($namespace) as $name => $element) {
                if (isset($array[$name])) {
                    if (!is_array($array[$name])) {
                        $array[$name] = [$array[$name]];
                    }
                    $array[$name][] = $element;
                } else {
                    $array[$name] = $element;
                }
            }
        }

        // json encode non-whitespace element simplexml text values.
        $text = trim($this);
        if (strlen($text)) {
            if ($array) {
                $array['@text'] = $text;
            } else {
                $array = $text;
            }
        }

        // return empty elements as NULL (self-closing or empty tags)
        if (!$array) {
            $array = NULL;
        }

        return $array;
    }

    public function toArray() {
        return (array) json_decode(json_encode($this));
    }
}

采用SimpleXML and JSON Encode in PHP – Part III and End中给出的更改JSON编码规则示例的命名空间。

答案 1 :(得分:0)

hakre给出的答案写得很好,正是我在寻找的东西,尤其是他最后提供的Feed类。但这在几种方面是不完整的,因此我修改了他的课程,使其更通用,并希望共享更改:

  • 最初遗漏的最重要的问题之一是属性也可能具有名称空间,如果不考虑名称空间,很可能会丢失元素上的属性。

  • 还有一点很重要,那就是在转换为数组时,如果某些内容可能包含相同名称但名称空间不同的元素,则无法分辨该元素来自哪个名称空间。 (是的,这是一种非常罕见的情况...但是我遇到了基于NIEM的政府标准...)因此我添加了一个静态选项,这将导致将名称空间前缀添加到最终数组中的所有键中属于一个名称空间。要使用它,设置 Feed::$withPrefix = true;,然后致电toArray()

  • 最后,根据我的个人喜好,我在toArray()中添加了一个选项,以使最终数组关联而不是使用对象。

这是更新的课程:

class Feed extends \SimpleXMLElement implements \JsonSerializable
{
    public static $withPrefix = false;

    public function jsonSerialize()
    {
        $array = array();
        $attributes = array();

        $namespaces = [null] + $this->getDocNamespaces(true);

        // json encode child elements if any. group on duplicate names as an array.
        foreach ($namespaces as $prefix => $namespace) {
            foreach ($this->attributes($namespace) as $name => $attribute) {
                if (static::$withPrefix && !empty($namespace)) {
                    $name = $prefix . ":" . $name;
                }
                $attributes[$name] = $attribute;
            }

            foreach ($this->children($namespace) as $name => $element) {
                if (static::$withPrefix && !empty($namespace)) {
                    $name = $prefix . ":" . $name;
                }
                if (isset($array[$name])) {
                    if (!is_array($array[$name])) {
                        $array[$name] = [$array[$name]];
                    }
                    $array[$name][] = $element;
                } else {
                    $array[$name] = $element;
                }
            }
        }

        if (!empty($attributes)) {
            $array['@attributes'] = $attributes;
        }

        // json encode non-whitespace element simplexml text values.
        $text = trim($this);
        if (strlen($text)) {
            if ($array) {
                $array['@text'] = $text;
            } else {
                $array = $text;
            }
        }

        // return empty elements as NULL (self-closing or empty tags)
        if (!$array) {
            $array = NULL;
        }

        return $array;
    }

    public function toArray($assoc=false) {
        return (array) json_decode(json_encode($this), $assoc);
    }
}