PHP解析XML/XBRL文件疑难解答与实战
2025-03-08 16:15:10
PHP 解析 XML/XBRL 文件:疑难解答与实战
碰上需要从 XML 或者 XBRL 文件里捞数据的活儿了?别慌,咱们来理一理。这事儿说难不难,说简单也不简单,关键是要找对路子。
问题:死活解析不出 XML/XBRL 文件
问题出在哪儿呢?通常,处理 XML/XBRL 文件时,会遇到下面几个坎儿:
- 文件结构复杂: XML 文件,尤其是 XBRL 文件,往往层层嵌套,跟俄罗斯套娃似的。直接用常规方法处理,容易迷路。
- 命名空间(Namespace): XML 文件里经常会用到命名空间来区分不同的标签和属性。这玩意儿处理不好,就会导致 XPath 查询失效。
- 数据类型和格式: XBRL 数据类型和格式跟普通的 XML 有区别,得按它的规矩来。
- 文件编码: 文件编码不一致会导致乱码。
几种搞定 XML/XBRL 文件的办法
针对上面的问题, 咱有以下解决方案,你可以挨个试试,总有一款适合你。
1. 使用 SimpleXML 扩展 (简单场景首选)
原理: 如果 XML 文件结构比较简单,没有太多复杂的嵌套和命名空间,那 SimpleXML 绝对是首选。它把 XML 文件直接转换成 PHP 对象,访问起来就跟访问对象的属性一样简单。
适用场景: 结构简单,没有命名空间,或者命名空间可以忽略的 XML 文件。
代码示例:
$xmlString = file_get_contents('http://regnskaber.virk.dk/48966882/ZG9rdW1lbnRsYWdlcjovLzAzLzdjLzk0LzFhL2I4LzY5ZGMtNGRhZi04NGE0LTRmNTEyN2UxY2U2MA.xml');
// 移除 BOM 头
$xmlString = str_replace("\xEF\xBB\xBF", '', $xmlString);
$xml = simplexml_load_string($xmlString);
if ($xml === false) {
echo "解析 XML 失败!";
exit;
}
// 举个例子: 获取公司名称 (根据实际情况修改)
// 因为有命名空间, 使用 SimpleXML 比较困难,需要通过 children() 方法指定
// 由于篇幅关系,此处暂时省略
//再举例,假设要获取 <fsa:Assets contextRef="c6">1694866</fsa:Assets> 的值
// 需要这么写,是不是很麻烦?所以这种场景 SimpleXML 并不是很好用
// echo $xml->children('fsa', true)->Assets;
安全提示: SimpleXML 默认会解析外部实体。如果有安全顾虑,可以用simplexml_load_string
函数的第二个参数, 设置为 LIBXML_NOENT
,禁用实体解析。
缺点:
- SimpleXML 对付复杂的 XML 比较吃力, 不方便精确控制, 处理复杂场景时会有点力不从心。
- SimpleXML 不能直接修改xml文档内容
2. 使用 DOMDocument 扩展 + XPath (复杂场景必备)
原理: DOMDocument
扩展将 XML 文件加载为一个树形结构,而 XPath 则像一个强大的“寻路器”,让你能够通过路径表达式精准地找到你想要的节点和数据。对于命名空间问题, 注册后使用就很容易处理了.
适用场景: 复杂的 XML 文件,尤其是那些有大量命名空间和嵌套结构的。
代码示例:
$xmldoc = new DOMDocument();
$xmlString = file_get_contents('http://regnskaber.virk.dk/48966882/ZG9rdW1lbnRsYWdlcjovLzAzLzdjLzk0LzFhL2I4LzY5ZGMtNGRhZi04NGE0LTRmNTEyN2UxY2U2MA.xml');
$xmlString = str_replace("\xEF\xBB\xBF",'',$xmlString); //去除 BOM 头
$xmldoc->loadXML($xmlString); // 使用 loadXML, 不是 load
$xpath = new DOMXPath($xmldoc);
// 注册命名空间, 每个都需要注册!
$xpath->registerNamespace('xbrli', 'http://www.xbrl.org/2003/instance');
$xpath->registerNamespace('fsa', 'http://xbrl.dcca.dk/fsa');
$xpath->registerNamespace('gsd', 'http://xbrl.dcca.dk/gsd');
$xpath->registerNamespace('cmn', 'http://xbrl.dcca.dk/cmn');
$xpath->registerNamespace('dst', 'http://xbrl.dcca.dk/dst');
$xpath->registerNamespace('arr', 'http://xbrl.dcca.dk/arr');
// 获取 Equity 的值
$equity = $xpath->query("//fsa:Equity[@contextRef='c142']");
if ($equity->length > 0) {
echo "Equity: " . $equity->item(0)->nodeValue . "\n";
}
// 获取 Assets 的值
$assets = $xpath->query("//fsa:Assets[@contextRef='c6']");
if ($assets->length > 0) {
echo "Assets: " . $assets->item(0)->nodeValue . "<br/>";
}
//获取公司名,在 //gsd:NameOfReportingEntity 中, 使用 text() 获取值
$companyName = $xpath->query("//gsd:NameOfReportingEntity/text()");
if ($companyName->length > 0) {
echo "CompanyName: " . $companyName->item(0)->nodeValue . "\n";
}
关键点解释:
$xmldoc->loadXML($xmlString)
: 这里使用loadXML
, 而不是load
,直接从字符串加载。$xpath->registerNamespace(...)
: 这一步非常关键!把 XML 文件里用到的命名空间都注册一下,查询的时候才能找对地方。注册的时候, 第一个参数是前缀,查询时需要使用这个前缀, 比如fsa:Equity
。第二个参数是 URL。$xpath->query(...)
: 使用 XPath 表达式来查询节点。//fsa:Equity[@contextRef='c142']
的意思就是:找到所有带有contextRef
属性且值为 'c142' 的fsa:Equity
节点。
安全提示: 和 SimpleXML
一样,DOMDocument
默认也会解析外部实体。出于安全考虑,建议在加载 XML 数据后,用$xmldoc->resolveExternals = false;
来禁用外部实体解析。
3. 使用 XMLReader 扩展(处理超大 XML 文件)
原理: 如果 XML 文件特别大,一次性加载到内存里可能会撑爆,这时候就轮到 XMLReader
登场了。它采用“流式处理”的方式,一点一点地读取 XML 文件,内存占用非常小。像个小溪流,不是一次全给你,而是一点点来。
适用场景: 处理大型 XML 文件,避免内存溢出。
代码示例:
$reader = new XMLReader();
$dataUrl = 'http://regnskaber.virk.dk/48966882/ZG9rdW1lbnRsYWdlcjovLzAzLzdjLzk0LzFhL2I4LzY5ZGMtNGRhZi04NGE0LTRmNTEyN2UxY2U2MA.xml';
$reader->open($dataUrl);
//注册需要使用的命名空间。
$namespaces = array(
'xbrli' => 'http://www.xbrl.org/2003/instance',
'fsa' => 'http://xbrl.dcca.dk/fsa',
'gsd' => 'http://xbrl.dcca.dk/gsd',
'cmn' => 'http://xbrl.dcca.dk/cmn',
'dst' => 'http://xbrl.dcca.dk/dst',
'arr' => 'http://xbrl.dcca.dk/arr'
);
//循环读取节点
while ($reader->read()) {
//如果是元素节点开始
if ($reader->nodeType == XMLReader::ELEMENT) {
//根据不同的节点名称进行解析
//检查命名空间,如果元素在命名空间中
if (isset($namespaces[$reader->namespaceURI])) {
$prefix = array_search($reader->namespaceURI, $namespaces);
if ($reader->localName == 'Equity' && $reader->getAttribute('contextRef') == 'c142') {
$reader->read(); // 移动到包含值的文本节点
if($reader->nodeType == XMLReader::TEXT ||$reader->nodeType == XMLReader::CDATA) {
echo "Equity (XMLReader): " . $reader->value . "\n";
}
} elseif ($reader->localName == 'Assets' && $reader->getAttribute('contextRef') == 'c6')
{
$reader->read(); //移动到下一个包含值的节点.
if($reader->nodeType == XMLReader::TEXT ||$reader->nodeType == XMLReader::CDATA){
echo "Assets (XMLReader): ".$reader->value ."\n";
}
}
}
}
}
$reader->close();
关键点解释:
$reader->read()
: 每次调用这个方法,XMLReader
就会读取 XML 文件中的下一个节点。$reader->nodeType
: 这个属性表示当前节点的类型,比如XMLReader::ELEMENT
表示元素节点,XMLReader::TEXT
表示文本节点。$reader->localName
: 当前节点的名称 (不含命名空间前缀)。$reader->namespaceURI
: 当前节点的命名空间。
安全提示: 和 DOMDocument 一样。 XMLReader
默认也会解析外部实体,记得处理。
4. 进阶使用技巧
- 错误处理: 一定要记得进行错误处理。 XML 解析出错是很常见的, 例如网络问题,格式问题等。用
libxml_use_internal_errors(true)
开启内部错误处理,然后用libxml_get_errors()
获取错误信息。 - XPath 表达式优化: XPath 表达式写得好,查询效率会大大提高。尽量使用具体的路径,避免使用
//
这种全局搜索。多利用属性和谓词来缩小查找范围。 - 缓存机制 : 如果需要多次读取相同的 XML 文件数据,可以考虑使用缓存机制(如 Memcached, Redis),减少重复解析的开销。
总结一下
上面介绍的三种方法各有千秋。要选择哪种方法,主要看 XML 文件的具体情况和你自己的需求:
- 如果结构简单: 用
SimpleXML
. - 如果结构复杂,需要灵活处理:
DOMDocument
+XPath
. - 如果文件巨大, 怕内存爆掉: 用
XMLReader
。
一般而言, 推荐使用DOMDocument
+XPath
的方案, 更强大和通用.