登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  php教程

PHP XMLReader 怎样把当前子树交给 DOM 继续处理

来源:17golang原创

时间:2026-11-10 07:20:26 293浏览 收藏

PHP XMLReader 适合沿着 XML 文档流向前读取,但它的游标接口并不适合每一种局部查询。当游标停在目标元素的起始节点时,可以调用 $reader->expand(new DOMDocument()),把当前节点及其子树复制成 DOM 节点,再交给 DOM API 读取属性、子节点或 XPath。这样既保留了 XMLReader 的流式入口,也只为当前业务对象建立一棵小 DOM 树。

官方地址:https://www.php.net/manual/en/xmlreader.expand.php

关键写法是先让 XMLReader 停在目标元素,再把这个“当前节点”展开到一个明确的 DOMDocument。expand() 返回的是复制出来的 DOMNode,失败时返回 false;它不会把整份 XML 自动变成 DOMDocument。

XMLReader 负责定位,DOM 负责局部处理

XMLReader 是向前移动的 XML Pull parser,当前游标一次只处在文档流中的一个节点。DOMDocument 则代表一棵完整的 XML 或 HTML 文档树。两者结合时,合理的分工是:XMLReader 负责从大文档中找到目标元素,DOM 负责在这个元素的小范围内做树形查询。

XMLReader::expand() 的语义是复制当前节点并返回相应的 DOM 对象;可选的 baseNode 参数用来指定新 DOM 对象所属的目标文档。传入 new DOMDocument('1.0', 'UTF-8'),可以让示例中的文档边界明确,也方便后续把节点交给 DOM 相关 API。

PHP XMLReader 当前子树通过 expand 复制到 DOMDocument 的静态关系图
图1:XMLReader 当前子树交给 DOMDocument 的静态关系图,不是运行截图。

先把游标停在目标元素

展开之前不要在文本节点、结束节点或属性节点上直接调用。最稳妥的判断是同时检查 nodeType 和 name:前者保证当前节点是元素起点,后者保证它就是需要交给 DOM 的业务元素。

键盘USB办公显示器4K';

$reader = XMLReader::fromString($xml);

while ($reader->read()) {
    // 只有元素起始节点才是可展开的目标,避免误处理文本和结束节点。
    if ($reader->nodeType !== XMLReader::ELEMENT || $reader->name !== 'product') {
        continue;
    }

    // 这里先停在 product,下一节再把当前子树交给 DOM。
    echo 'found: ' . $reader->getAttribute('sku') . PHP_EOL;
}

// 输入流处理完成后显式关闭读取器。
$reader->close();

这段定位逻辑没有调用 readOuterXml(),也没有把全量 XML 交给 DOMDocument::loadXML()。目标是让后续操作只围绕当前 product 节点展开。

用 expand() 复制当前节点

定位到目标元素后,给 expand() 传入一个新的 DOMDocument。返回值可能是 DOMNode,也可能是 false,因此要把失败分支当成正常的输入或解析错误处理。

expand($targetDocument);

    // expand() 失败时返回 false,不能把它当成可访问的 DOMElement。
    if ($node === false) {
        return null;
    }

    return $node;
}

$reader = XMLReader::fromString($xml);
while ($reader->read()) {
    // 先确认当前是目标元素,再进行一次局部展开。
    if ($reader->nodeType !== XMLReader::ELEMENT || $reader->name !== 'product') {
        continue;
    }

    $productNode = expandCurrentProduct($reader);
    if ($productNode === null) {
        // 记录当前记录失败后继续读后面的 XML,避免整个批次静默中断。
        fwrite(STDERR, "product expand failed\n");
        continue;
    }

    // expand 返回的节点可按 DOMNode 使用,元素节点再读取属性和子节点。
    $sku = $productNode instanceof DOMElement ? $productNode->getAttribute('sku') : '';
    echo $sku . PHP_EOL;
}

// 明确释放流式读取资源。
$reader->close();

传入 DOMDocument 的价值在于给复制节点一个清晰的目标文档。不要把这个动作理解成“把 reader 转成 DOMReader”:原来的 XMLReader 仍然是流式游标,DOM 节点只是当前子树的一份副本。

PHP XMLReader 流式游标与局部 DOM 树分工的静态说明图
图2:流式游标与局部 DOM 树的分工说明图,不是浏览器或编辑器截图。

在局部 DOM 上读取属性、子节点和 XPath

展开之后,可以选择最简单的 DOM 方法,也可以创建 DOMXPath 做更复杂的局部查询。因为查询范围已经缩小到当前 product,XPath 不必面对整份输入文档。

getElementsByTagName('name')->item(0);
    $dom = $productNode->ownerDocument;
    $xpath = $dom instanceof DOMDocument ? new DOMXPath($dom) : null;
    $tagNodes = $xpath === null
        ? []
        : $xpath->query('./tags/tag', $productNode);

    $tags = [];
    if ($tagNodes !== false) {
        foreach ($tagNodes as $tagNode) {
            // 只收集当前 product 的 tag 文本,不读取兄弟 product。
            $tags[] = trim($tagNode->textContent);
        }
    }

    return [
        'sku' => $productNode->getAttribute('sku'),
        'name' => $nameNode instanceof DOMNode ? trim($nameNode->textContent) : '',
        'tags' => $tags,
    ];
}

这里的 ./tags/tag 是相对当前节点的查询,能够把“当前子树处理”表达得更清楚。若 XML 使用命名空间,应在 DOMXPath 上注册对应前缀,再用带前缀的 XPath 查询;不能因为 local-name() 方便就忽略命名空间语义。

继续遍历时,read() 和 next() 要分工

read() 沿文档流移动到下一个节点,适合需要观察文本、嵌套元素或结束节点的细粒度遍历。next() 则用于跳过当前元素的整个子树,寻找后续指定名称的元素,适合“每个 product 独立处理、无需再看其内部流事件”的场景。

两者不要在同一个循环里随意叠加。若当前元素已经通过 expand() 处理完毕,继续使用外层 read() 会让代码按节点粒度前进;若改成 next('product'),就应围绕“next 返回的目标元素”重新组织循环,避免下一次 read() 又把刚找到的目标元素跳过去。

目标适合的方法要注意的边界
观察下一个 XML 节点read()可能进入当前元素的子节点和结束节点
跳过当前元素的整棵子树next()不要再用无条件 read() 把返回的目标元素跳过
把当前节点交给 DOMexpand()返回 DOMNode 或 false,只复制当前节点及其子树
读取局部树DOMElement、DOMXPath查询范围应从当前节点出发,命名空间要显式处理

内存和异常边界怎么处理

  • 不要长期保存 DOMNode:处理完一个目标元素后,只保留已经提取出的标量或数组,把节点变量交给下一轮覆盖。
  • 展开失败要有分支:expand() 返回 false 时记录上下文并决定跳过、重试输入或终止任务,不要继续调用 getAttribute()。
  • 目标元素要足够小:如果当前节点本身包含超大的二进制文本或深层日志,expand 仍会复制这棵子树;流式读取不是局部展开后的内存保证。
  • 编码统一:XMLReader 文档说明内部使用 UTF-8,业务输出前仍要根据接口或文件协议决定是否转换编码。

常见问题

expand() 会不会把整份 XML 都加载进 DOM?

不会。它复制的是 XMLReader 当前节点及其子树;但如果当前节点本身很大,复制这棵局部树仍然可能占用明显内存。

为什么 expand() 后还要传 DOMDocument?

可选的 baseNode 用来指定新 DOM 对象的目标文档。传入一个明确的 DOMDocument,后续调用 ownerDocument、DOMXPath 或导入其他 DOM 节点时更容易保持文档边界清晰。

能不能直接用 readOuterXml() 替代 expand()?

可以把外层 XML 文本读出来再交给其他解析器,但那条路径需要重新解析字符串;当后续逻辑已经是 DOM API 时,expand() 直接得到 DOMNode,代码更短,也少一次序列化和解析。

什么时候只用 XMLReader,不要展开?

如果只需要读取属性、文本或固定层级的少量字段,继续使用 XMLReader 往往更省内存。只有当当前子树需要 XPath、兄弟关系、深层节点查找或 DOM 生态接口时,再把这一小段交给 DOM。

可以把这套组合记成一句话:XMLReader 负责在大文档里找对位置,expand() 负责复制当前位置,DOMDocument 和 DOMXPath 负责把这一小段树处理干净;处理完后再让 XMLReader 继续向前。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>