返回

PHP解析XML/XBRL文件疑难解答与实战

php

PHP 解析 XML/XBRL 文件:疑难解答与实战

碰上需要从 XML 或者 XBRL 文件里捞数据的活儿了?别慌,咱们来理一理。这事儿说难不难,说简单也不简单,关键是要找对路子。

问题:死活解析不出 XML/XBRL 文件

问题出在哪儿呢?通常,处理 XML/XBRL 文件时,会遇到下面几个坎儿:

  1. 文件结构复杂: XML 文件,尤其是 XBRL 文件,往往层层嵌套,跟俄罗斯套娃似的。直接用常规方法处理,容易迷路。
  2. 命名空间(Namespace): XML 文件里经常会用到命名空间来区分不同的标签和属性。这玩意儿处理不好,就会导致 XPath 查询失效。
  3. 数据类型和格式: XBRL 数据类型和格式跟普通的 XML 有区别,得按它的规矩来。
  4. 文件编码: 文件编码不一致会导致乱码。

几种搞定 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 的方案, 更强大和通用.