XMLReader / XMLWriter
XMLReader 和 XMLWriter 是 PHP 提供的流式 XML 处理扩展。与 DOM 和 SimpleXML 将整个文档加载到内存不同,XMLReader 采用事件驱动的方式逐节点读取,XMLWriter 以流式方式写入。它们适合处理大体积 XML 文件,内存占用极低。
基础概念
为什么使用流式处理
| 方式 | 内存占用 | 适用场景 | 读写能力 |
|---|---|---|---|
| DOM | 高(全量加载) | 需要随机访问 | 读 + 写 |
| SimpleXML | 高(全量加载) | 简单读写 | 读 + 写 |
| XMLReader | 极低(逐节点) | 大文件读取 | 仅读 |
| XMLWriter | 极低(流式写入) | 生成大文件 | 仅写 |
安装
bash
# XMLReader 和 XMLWriter 通常是 PHP 默认启用的
./configure --enable-xmlreader --enable-xmlwriterXMLReader
基本读取
php
<?php
declare(strict_types=1);
$reader = new XMLReader();
// 从文件打开
$reader->open('large-data.xml');
// 从字符串打开
// $reader->XML($xmlString);
// 节点类型常量
// XMLReader::ELEMENT - 元素开始
// XMLReader::END_ELEMENT - 元素结束
// XMLReader::TEXT - 文本
// XMLReader::ATTRIBUTE - 属性
// XMLReader::COMMENT - 注释
// XMLReader::CDATA - CDATA 节
// XMLReader::DOCUMENT - 文档节点
while ($reader->read()) {
switch ($reader->nodeType) {
case XMLReader::ELEMENT:
echo "开始元素: <{$reader->name}>" . PHP_EOL;
// 读取属性
if ($reader->hasAttributes) {
while ($reader->moveToNextAttribute()) {
echo " 属性: {$reader->name} = {$reader->value}" . PHP_EOL;
}
$reader->moveToElement(); // 回到元素
}
break;
case XMLReader::END_ELEMENT:
echo "结束元素: </{$reader->name}>" . PHP_EOL;
break;
case XMLReader::TEXT:
echo "文本内容: " . trim($reader->value) . PHP_EOL;
break;
case XMLReader::COMMENT:
echo "注释: " . $reader->value . PHP_EOL;
break;
case XMLReader::CDATA:
echo "CDATA: " . $reader->value . PHP_EOL;
break;
}
}
$reader->close();读取指定元素
php
<?php
declare(strict_types=1);
/**
* 使用 read() 和 next() 精确定位元素
*/
function parseBooks(string $filePath): array
{
$reader = new XMLReader();
$reader->open($filePath);
$books = [];
// 跳到第一个 <book> 元素
while ($reader->read()) {
if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'book') {
$book = [
'id' => $reader->getAttribute('id'),
'category' => $reader->getAttribute('category'),
'title' => '',
'author' => '',
'year' => 0,
'price' => 0.0,
];
// 读取当前元素的子节点
while ($reader->read()) {
switch ($reader->nodeType) {
case XMLReader::ELEMENT:
$name = $reader->name;
if ($reader->isEmptyElement) {
break;
}
$reader->read(); // 移到文本节点
$value = trim($reader->value);
$book[$name] = match ($name) {
'year' => (int) $value,
'price' => (float) $value,
default => $value,
};
break;
case XMLReader::END_ELEMENT:
if ($reader->name === 'book') {
$books[] = $book;
break 2; // 跳出内层循环,继续读取下一个 book
}
break;
}
}
}
}
$reader->close();
return $books;
}
// 使用示例
// $books = parseBooks('library.xml');
// print_r($books);使用 expand() 与 SimpleXML/DOM 配合
php
<?php
declare(strict_types=1);
/**
* XMLReader + SimpleXML 混合使用
* 只对需要的节点展开为 SimpleXMLElement
*/
function parseLargeXml(string $filePath): Generator
{
$reader = new XMLReader();
$reader->open($filePath);
while ($reader->read()) {
if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'book') {
// 将当前节点及其子树展开为 SimpleXMLElement
$node = $reader->expand();
if ($node instanceof DOMElement) {
$simpleXml = simplexml_import_dom($node);
yield [
'id' => (string) $simpleXml['id'],
'title' => (string) $simpleXml->title,
'author' => (string) $simpleXml->author,
'price' => (float) $simpleXml->price,
];
}
}
}
$reader->close();
}
// 使用示例
foreach (parseLargeXml('library.xml') as $book) {
echo "{$book['title']}: {$book['price']}" . PHP_EOL;
}XMLReader 的高级方法
php
<?php
declare(strict_types=1);
$reader = new XMLReader();
$reader->open('data.xml');
// moveToNextAttribute - 移到下一个属性
// moveToAttribute($name) - 移到指定属性
// moveToAttributeNo($index) - 移到指定索引的属性
// moveToElement - 回到当前元素
// moveToFirstAttribute - 移到第一个属性
// moveToParent - 移到父节点
// next($localName) - 移到同级下一个元素
// readInnerXml - 读取内部 XML(不包含当前标签)
// readOuterXml - 读取外部 XML(包含当前标签)
while ($reader->read()) {
if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'content') {
$innerXml = $reader->readInnerXml();
$outerXml = $reader->readOuterXml();
echo "内部: {$innerXml}" . PHP_EOL;
echo "外部: {$outerXml}" . PHP_EOL;
}
}
$reader->close();XMLReader 选项
php
<?php
declare(strict_types=1);
$reader = new XMLReader();
// 设置选项
$reader->setParserProperty(XMLReader::SUBST_ENTITIES, true);
$reader->setRelaxNGSchema('schema.rng');
$reader->setSchema('schema.xsd');
// 打开时使用 libxml 选项
$reader->open('data.xml', null, LIBXML_NOBLANKS | LIBXML_NOENT);
// 验证文档
if ($reader->setSchema('schema.xsd')) {
while ($reader->read()) {
// 处理数据
}
$reader->close();
if (!$reader->isValid()) {
echo "XML 文档验证失败" . PHP_EOL;
}
}XMLWriter
基本写入
php
<?php
declare(strict_types=1);
/**
* 使用 XMLWriter 生成 XML
*/
function writeXml(string $filePath): void
{
$writer = new XMLWriter();
// 写入文件
$writer->openURI($filePath);
// 或写入内存
// $writer->openMemory();
// 启用缩进
$writer->setIndent(true);
$writer->setIndentString(' ');
// 开始文档
$writer->startDocument('1.0', 'UTF-8');
// 根元素
$writer->startElement('bookstore');
// 书籍 1
$writer->startElement('book');
$writer->writeAttribute('id', '1');
$writer->writeAttribute('category', 'fiction');
$writer->writeElement('title', 'Harry Potter');
$writer->writeElement('author', 'J.K. Rowling');
$writer->writeElement('year', '2005');
$writer->writeElement('price', '29.99');
$writer->endElement(); // book
// 书籍 2
$writer->startElement('book');
$writer->writeAttribute('id', '2');
$writer->writeAttribute('category', 'programming');
$writer->writeElement('title', 'Clean Code');
$writer->writeElement('author', 'Robert C. Martin');
$writer->writeElement('year', '2008');
$writer->writeElement('price', '39.99');
$writer->endElement(); // book
$writer->endElement(); // bookstore
// 结束文档
$writer->endDocument();
$writer->flush();
echo "XML 文件已写入: {$filePath}" . PHP_EOL;
}
// 使用示例
// writeXml('/tmp/bookstore.xml');写入内存
php
<?php
declare(strict_types=1);
function generateXmlString(): string
{
$writer = new XMLWriter();
$writer->openMemory();
$writer->setIndent(true);
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement('data');
$writer->writeElement('name', '张三');
$writer->writeElement('age', '30');
$writer->endElement();
$writer->endDocument();
return $writer->outputMemory(); // 获取内存中的 XML 字符串
}
echo generateXmlString();写入 CDATA、注释和 PI
php
<?php
declare(strict_types=1);
$writer = new XMLWriter();
$writer->openMemory();
$writer->setIndent(true);
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement('root');
// 注释
$writer->writeComment('这是一个配置文件');
// CDATA
$writer->startElement('description');
$writer->writeCData('<strong>HTML 内容</strong>');
$writer->endElement();
// 处理指令(PI)
$writer->writePi('xml-stylesheet', 'type="text/xsl" href="style.xsl"');
// 文本(支持 rawText 8.2+)
$writer->startElement('code');
$writer->writeRaw('<expression>');
$writer->endElement();
$writer->endElement();
$writer->endDocument();
echo $writer->outputMemory();实战示例
大文件转换(XMLReader + XMLWriter)
php
<?php
declare(strict_types=1);
/**
* 使用 XMLReader 读取 + XMLWriter 写入进行 XML 转换
* 内存占用极低,适合处理 GB 级 XML 文件
*/
function transformXml(
string $inputFile,
string $outputFile,
callable $transformer
): void {
$reader = new XMLReader();
$writer = new XMLWriter();
$reader->open($inputFile);
$writer->openURI($outputFile);
$writer->setIndent(true);
$writer->startDocument('1.0', 'UTF-8');
while ($reader->read()) {
switch ($reader->nodeType) {
case XMLReader::ELEMENT:
$writer->startElement($reader->name);
// 复制属性(可能经过修改)
if ($reader->hasAttributes) {
while ($reader->moveToNextAttribute()) {
$writer->writeAttribute($reader->name, $reader->value);
}
$reader->moveToElement();
}
// 空元素
if ($reader->isEmptyElement) {
$writer->endElement();
}
break;
case XMLReader::END_ELEMENT:
$writer->endElement();
break;
case XMLReader::TEXT:
case XMLReader::CDATA:
$writer->text($reader->value);
break;
case XMLReader::COMMENT:
$writer->writeComment($reader->value);
break;
}
}
$writer->endDocument();
$writer->flush();
$reader->close();
echo "转换完成: {$outputFile}" . PHP_EOL;
}
// 使用示例:将所有价格乘以 0.8
// transformXml('input.xml', 'output.xml', function ($name, $value) {
// return $name === 'price' ? (float)$value * 0.8 : $value;
// });XML 导入工具(CSV 转 XML)
php
<?php
declare(strict_types=1);
/**
* CSV 转 XML(使用 XMLWriter 流式写入)
*/
function csvToXml(
string $csvPath,
string $xmlPath,
string $rootTag = 'records',
string $rowTag = 'record'
): void {
$csvFile = fopen($csvPath, 'r');
if ($csvFile === false) {
throw new RuntimeException("无法打开 CSV 文件");
}
$headers = fgetcsv($csvFile);
if ($headers === false) {
fclose($csvFile);
throw new RuntimeException("CSV 文件为空");
}
$writer = new XMLWriter();
$writer->openURI($xmlPath);
$writer->setIndent(true);
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement($rootTag);
$rowCount = 0;
while (($row = fgetcsv($csvFile)) !== false) {
$writer->startElement($rowTag);
foreach ($headers as $i => $header) {
$value = $row[$i] ?? '';
$writer->writeElement($header, $value);
}
$writer->endElement();
$rowCount++;
}
$writer->endElement();
$writer->endDocument();
$writer->flush();
fclose($csvFile);
echo "转换完成: {$rowCount} 条记录" . PHP_EOL;
}
// 使用示例
// csvToXml('data.csv', 'data.xml', 'users', 'user');注意事项
XMLReader 注意事项
read()返回 false 表示读取失败,返回 true 表示有更多节点- 使用
expand()时需要足够内存来保存展开的子树 - 空元素(如
<br/>)在 XMLReader 中isEmptyElement为 true
XMLWriter 注意事项
- 必须确保
startElement和endElement配对 - 使用
flush()定期将缓冲区内容写入文件 openURI()支持文件路径和 PHP 流包装器
最佳实践
- 大文件用 XMLReader:避免 DOM/SimpleXML 的内存问题
- XMLReader + XMLWriter 组合:处理 XML 转换的内存效率最高
- 使用 expand() 精确提取:只对需要的子树展开
- XMLWriter 启用缩进:便于调试和阅读
- 错误处理:检查
open()、read()的返回值
下一节
继续学习:XSL 转换