Skip to content

XMLReader / XMLWriter

XMLReader 和 XMLWriter 是 PHP 提供的流式 XML 处理扩展。与 DOM 和 SimpleXML 将整个文档加载到内存不同,XMLReader 采用事件驱动的方式逐节点读取,XMLWriter 以流式方式写入。它们适合处理大体积 XML 文件,内存占用极低。

前置知识

阅读本节前,建议先了解:DOM 操作SimpleXML

基础概念

为什么使用流式处理

方式内存占用适用场景读写能力
DOM高(全量加载)需要随机访问读 + 写
SimpleXML高(全量加载)简单读写读 + 写
XMLReader极低(逐节点)大文件读取仅读
XMLWriter极低(流式写入)生成大文件仅写

安装

bash
# XMLReader 和 XMLWriter 通常是 PHP 默认启用的
./configure --enable-xmlreader --enable-xmlwriter

XMLReader

基本读取

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 注意事项

  • 必须确保 startElementendElement 配对
  • 使用 flush() 定期将缓冲区内容写入文件
  • openURI() 支持文件路径和 PHP 流包装器

最佳实践

  1. 大文件用 XMLReader:避免 DOM/SimpleXML 的内存问题
  2. XMLReader + XMLWriter 组合:处理 XML 转换的内存效率最高
  3. 使用 expand() 精确提取:只对需要的子树展开
  4. XMLWriter 启用缩进:便于调试和阅读
  5. 错误处理:检查 open()read() 的返回值

下一节

继续学习:XSL 转换

参考链接