将XML转换为JSON的PHP方法

193

我正在尝试在php中将xml转换为json。如果我使用simple xml和json_encode进行简单的转换,那么xml中的属性将不会显示。

$xml = simplexml_load_file("states.xml");
echo json_encode($xml);

因此,我正在尝试像这样手动解析它。

foreach($xml->children() as $state)
{
    $states[]= array('state' => $state->name); 
}       
echo json_encode($states);

问题是什么?

状态输出为{"state":{"0":"Alabama"}}而不是{"state":"Alabama"}

我做错了什么?

XML:

<?xml version="1.0" ?>
<states>
    <state id="AL">     
    <name>Alabama</name>
    </state>
    <state id="AK">
        <name>Alaska</name>
    </state>
</states>

输出:

[{"state":{"0":"Alabama"}},{"state":{"0":"Alaska"}

变量转储:

object(SimpleXMLElement)#1 (1) {
["state"]=>
array(2) {
[0]=>
object(SimpleXMLElement)#3 (2) {
  ["@attributes"]=>
  array(1) {
    ["id"]=>
    string(2) "AL"
  }
  ["name"]=>
  string(7) "Alabama"
}
[1]=>
object(SimpleXMLElement)#2 (2) {
  ["@attributes"]=>
  array(1) {
    ["id"]=>
    string(2) "AK"
  }
  ["name"]=>
  string(6) "Alaska"
}
}
}

请包含XML片段和解析后的最终数组结构。 (var_dump可以正常工作。) - nikc.org
添加了输入、输出和 var_dump。 - Bryan Hadlock
有些应用程序需要“完美的XML-to-JSON映射”,也就是jsonML,请参见此处的解决方案 - Peter Krauss
23个回答

549

3行代码从XML中获取Json和数组:

$xml = simplexml_load_string($xml_string);
$json = json_encode($xml);
$array = json_decode($json,TRUE);

67
这个解决方案并非完美,它完全舍弃了XML属性。因此,<person my-attribute='name'>John</person> 被解释为 <person>John</person> - Jake Wilson
17
为了使cdata元素变得平滑,可以使用$xml = simplexml_load_string($xml_string,'SimpleXMLElement',LIBXML_NOCDATA);。该语句可以将XML字符串加载为SimpleXMLElement对象,并去除其中的CDATA元素。 - txyoji
38
@JakeWilson也许是由于已经过去的两年以及各种版本修复,但在PHP 5.6.30中,这个方法会产生所有数据。属性存储在数组中的@attributes键下,所以它的表现非常完美和优美。三行简短的代码完美地解决了我的问题。 - Alex
2
如果您有多个命名空间,则此方法无法正常工作,您只能选择一个命名空间,并将其传递到 $json_string 中。:'( - jirislav
1
请注意,使用此解决方案时,当可能存在多个具有相同名称的节点时,一个节点将导致键仅指向一个元素,但是多个节点将导致键指向元素的_array_:<list><item><a>123</a><a>456</a></item><item><a>123</a></item></list> -> {"item":[{"a":["123","456"]},{"a":"123"}]}。php.net上的一个解决方案由ratfactor提供通过始终将元素存储在数组中来解决该问题。 - Klesun
显示剩余8条评论

42

很抱歉回复一个旧的帖子,但是这篇文章介绍了一种相对简短、简洁和易于维护的方法。我自己测试过,效果相当不错。

http://lostechies.com/seanbiefeld/2011/10/21/simple-xml-to-json-with-php/

<?php   
class XmlToJson {
    public function Parse ($url) {
        $fileContents= file_get_contents($url);
        $fileContents = str_replace(array("\n", "\r", "\t"), '', $fileContents);
        $fileContents = trim(str_replace('"', "'", $fileContents));
        $simpleXml = simplexml_load_string($fileContents);
        $json = json_encode($simpleXml);

        return $json;
    }
}
?>

7
如果您的 XML 中有多个相同标签的实例,这种方法将无法正常工作。json_encode 只会对该标签的最后一个实例进行序列化。 - ethree
2
对于所有查看此旧答案的人:请记住这些内容是在过去编写的,也许考虑更现代化的方法。 - Coreus

37

我明白了。json_encode对对象的处理与字符串不同。我将对象强制转换为字符串,现在它可以正常工作了。

foreach($xml->children() as $state)
{
    $states[]= array('state' => (string)$state->name); 
}       
echo json_encode($states);

22

我想我有点晚来参加这个派对,但我写了一个小函数来完成这个任务。它还处理属性、文本内容,甚至处理具有相同节点名称的多个兄弟节点。

免责声明: 我不是PHP本地人,所以请容忍简单的错误。

function xml2js($xmlnode) {
    $root = (func_num_args() > 1 ? false : true);
    $jsnode = array();

    if (!$root) {
        if (count($xmlnode->attributes()) > 0){
            $jsnode["$"] = array();
            foreach($xmlnode->attributes() as $key => $value)
                $jsnode["$"][$key] = (string)$value;
        }

        $textcontent = trim((string)$xmlnode);
        if (count($textcontent) > 0)
            $jsnode["_"] = $textcontent;

        foreach ($xmlnode->children() as $childxmlnode) {
            $childname = $childxmlnode->getName();
            if (!array_key_exists($childname, $jsnode))
                $jsnode[$childname] = array();
            array_push($jsnode[$childname], xml2js($childxmlnode, true));
        }
        return $jsnode;
    } else {
        $nodename = $xmlnode->getName();
        $jsnode[$nodename] = array();
        array_push($jsnode[$nodename], xml2js($xmlnode, true));
        return json_encode($jsnode);
    }
}   

使用示例:

$xml = simplexml_load_file("myfile.xml");
echo xml2js($xml);

示例输入(myfile.xml):

<family name="Johnson">
    <child name="John" age="5">
        <toy status="old">Trooper</toy>
        <toy status="old">Ultrablock</toy>
        <toy status="new">Bike</toy>
    </child>
</family>

输出示例:

{"family":[{"$":{"name":"Johnson"},"child":[{"$":{"name":"John","age":"5"},"toy":[{"$":{"status":"old"},"_":"Trooper"},{"$":{"status":"old"},"_":"Ultrablock"},{"$":{"status":"new"},"_":"Bike"}]}]}]}

美化后的输出:

{
    "family" : [{
            "$" : {
                "name" : "Johnson"
            },
            "child" : [{
                    "$" : {
                        "name" : "John",
                        "age" : "5"
                    },
                    "toy" : [{
                            "$" : {
                                "status" : "old"
                            },
                            "_" : "Trooper"
                        }, {
                            "$" : {
                                "status" : "old"
                            },
                            "_" : "Ultrablock"
                        }, {
                            "$" : {
                                "status" : "new"
                            },
                            "_" : "Bike"
                        }
                    ]
                }
            ]
        }
    ]
}

需要注意的问题: 相同标签名的标签可以是兄弟节点。其他解决方案很可能会删除除了最后一个兄弟节点以外的所有节点。为了避免这种情况,每个节点都是一个数组,即使它只有一个子节点,也是一个数组,其中包含每个标签实例的对象。(请参见示例中的多个“”元素)

甚至根元素,在有效的XML文档中只应存在一个,也被存储为具有实例对象的数组,以保持一致的数据结构。

为了能够区分XML节点内容和XML属性,每个对象的属性都存储在“$”中,而内容存储在“_”子元素中。

编辑: 我忘记显示您的示例输入数据的输出

{
    "states" : [{
            "state" : [{
                    "$" : {
                        "id" : "AL"
                    },
                    "name" : [{
                            "_" : "Alabama"
                        }
                    ]
                }, {
                    "$" : {
                        "id" : "AK"
                    },
                    "name" : [{
                            "_" : "Alaska"
                        }
                    ]
                }
            ]
        }
    ]
}

它能解析大型XML数据吗? - Volatil3
2
这个解决方案更好,因为它不会丢弃XML属性。请参见http://www.xml.com/lpt/a/1658中的“半结构化XML”,了解为什么这种复杂结构比简化结构更好...哦,对于CDATA,正如@txyoji建议的那样,可以使用`$xml = simplexml_load_file("myfile.xml",'SimpleXMLElement',LIBXML_‌​NOCDATA);`来展开CDATA元素。 - Peter Krauss
非常感谢您提供的自定义函数!它使得调整变得非常容易。顺便说一下,我添加了一个编辑过的版本,以 JS 的方式解析 XML:每个条目都有自己的对象(如果它们具有相同的标签名称,则不会将条目存储在单个数组中),因此顺序得到保留。 - lucifer63
1
错误 致命错误:未捕获的错误:在布尔值上调用成员函数getName() .. 我认为PHP版本出了问题 :-( .. 请帮忙! - KingRider

14

一个常见的错误是忘记 json_encode() 函数不会尊重同时存在文本值属性的元素。它只会选择其中一个,这意味着数据丢失。

下面的函数解决了这个问题。如果决定使用 json_encode/decode 方法,请使用以下函数。

function json_prepare_xml($domNode) {
  foreach($domNode->childNodes as $node) {
    if($node->hasChildNodes()) {
      json_prepare_xml($node);
    } else {
      if($domNode->hasAttributes() && strlen($domNode->nodeValue)){
         $domNode->setAttribute("nodeValue", $node->textContent);
         $node->nodeValue = "";
      }
    }
  }
}

$dom = new DOMDocument();
$dom->loadXML( file_get_contents($xmlfile) );
json_prepare_xml($dom);
$sxml = simplexml_load_string( $dom->saveXML() );
$json = json_decode( json_encode( $sxml ) );

这样做,<foo bar="3">Lorem</foo>在你的JSON中将不会变成{"foo":"Lorem"}


如果纠正语法错误,它仍然不能编译并且也不能产生所描述的输出。 - Richard Kiefer
$dom是什么?它从哪里来的? - Jake Wilson
$dom = new DOMDocument(); 这是它的来源。 - Scott
1
最后一行代码应该是:$json = json_decode( json_encode( $sxml ) ); - Lawrence Cooke

10

如果你的XML是SOAP文件,你可以使用以下代码:

$xmlStr = preg_replace("/(<\/?)(\w+):([^>]*>)/", "$1$2$3", $xmlStr);
$xml = new SimpleXMLElement($xmlStr);
return json_encode($xml);

1
这是非常重要的一点,SOAP用户现在就可以尝试,它仍然有效到2023年。 - AdamJones

7

试着使用这个

$xml = ... // Xml file data

// first approach
$Json = json_encode(simplexml_load_string($xml));

---------------- OR -----------------------

// second approach
$Json = json_encode(simplexml_load_string($xml, "SimpleXMLElement", LIBXML_NOCDATA));

echo $Json;

或者你可以使用这个库:https://github.com/rentpost/xml2array


该库可用于将XML转换为PHP数组,让数据处理更加方便。

5

最佳解决方案,像魔法一样运行。

$fileContents= file_get_contents($url);

$fileContents = str_replace(array("\n", "\r", "\t"), '', $fileContents);

$fileContents = trim(str_replace('"', "'", $fileContents));

$simpleXml = simplexml_load_string($fileContents);

//$json = json_encode($simpleXml); // Remove // if you want to store the result in $json variable

echo '<pre>'.json_encode($simpleXml,JSON_PRETTY_PRINT).'</pre>';

Source


4

这个解决方案处理命名空间,属性,并且对于重复的元素产生一致的结果(始终是数组,即使只有一个实例)。 受到 ratfactor 的 sxiToArray() 的启发。

/**
 * <root><a>5</a><b>6</b><b>8</b></root> -> {"root":[{"a":["5"],"b":["6","8"]}]}
 * <root a="5"><b>6</b><b>8</b></root> -> {"root":[{"a":"5","b":["6","8"]}]}
 * <root xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"><a>123</a><wsp:b>456</wsp:b></root> 
 *   -> {"root":[{"xmlns:wsp":"http://schemas.xmlsoap.org/ws/2004/09/policy","a":["123"],"wsp:b":["456"]}]}
 */
function domNodesToArray(array $tags, \DOMXPath $xpath)
{
    $tagNameToArr = [];
    foreach ($tags as $tag) {
        $tagData = [];
        $attrs = $tag->attributes ? iterator_to_array($tag->attributes) : [];
        $subTags = $tag->childNodes ? iterator_to_array($tag->childNodes) : [];
        foreach ($xpath->query('namespace::*', $tag) as $nsNode) {
            // the only way to get xmlns:*, see https://dev59.com/3kzSa4cB1Zd3GeqPoaVx#2470433
            if ($tag->hasAttribute($nsNode->nodeName)) {
                $attrs[] = $nsNode;
            }
        }

        foreach ($attrs as $attr) {
            $tagData[$attr->nodeName] = $attr->nodeValue;
        }
        if (count($subTags) === 1 && $subTags[0] instanceof \DOMText) {
            $text = $subTags[0]->nodeValue;
        } elseif (count($subTags) === 0) {
            $text = '';
        } else {
            // ignore whitespace (and any other text if any) between nodes
            $isNotDomText = function($node){return !($node instanceof \DOMText);};
            $realNodes = array_filter($subTags, $isNotDomText);
            $subTagNameToArr = domNodesToArray($realNodes, $xpath);
            $tagData = array_merge($tagData, $subTagNameToArr);
            $text = null;
        }
        if (!is_null($text)) {
            if ($attrs) {
                if ($text) {
                    $tagData['_'] = $text;
                }
            } else {
                $tagData = $text;
            }
        }
        $keyName = $tag->nodeName;
        $tagNameToArr[$keyName][] = $tagData;
    }
    return $tagNameToArr;
}

function xmlToArr(string $xml)
{
    $doc = new \DOMDocument();
    $doc->loadXML($xml);
    $xpath = new \DOMXPath($doc);
    $tags = $doc->childNodes ? iterator_to_array($doc->childNodes) : [];
    return domNodesToArray($tags, $xpath);
}

例子:

php > print(json_encode(xmlToArr('<root a="5"><b>6</b></root>')));
{"root":[{"a":"5","b":["6"]}]}

这实际上适用于多命名空间的情况,比其他解决方案更好,为什么会被踩呢... - aaron
在尝试了数十种解决方案后,这个是唯一一个对我有效的,非常感谢! - G Chris DCosta

3

我曾使用Miles Johnson的TypeConverter来实现这个目的。它可以使用Composer进行安装。

你可以使用它来编写类似于以下内容的代码:

<?php
require 'vendor/autoload.php';
use mjohnson\utility\TypeConverter;

$xml = file_get_contents("file.xml");
$arr = TypeConverter::xmlToArray($xml, TypeConverter::XML_GROUP);
echo json_encode($arr);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接