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。

先把游标停在目标元素
展开之前不要在文本节点、结束节点或属性节点上直接调用。最稳妥的判断是同时检查 nodeType 和 name:前者保证当前节点是元素起点,后者保证它就是需要交给 DOM 的业务元素。
键盘 USB 办公 '; $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(); 显示器 4K
这段定位逻辑没有调用 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 节点只是当前子树的一份副本。

在局部 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() 把返回的目标元素跳过 |
| 把当前节点交给 DOM | expand() | 返回 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 继续向前。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
163 收藏
-
381 收藏
-
178 收藏
-
331 收藏
-
384 收藏
-
399 收藏
-
378 收藏
-
219 收藏
-
204 收藏
-
447 收藏
-
140 收藏
-
109 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习