XML Expat Parser

Event-driven SAX parsing: constant memory, ideal for large files.

Intermediate
Key PointsExpat fires callbacks as it reads without building a tree, so even tens of MB stay in memory.

Three approaches

ApproachModelMemoryBest for
Expat (SAX)事件回调,流式恒定大文件、只要提取部分数据
DOM整棵树与文件大小成正比要改结构、要 XPath
SimpleXML对象映射与文件大小成正比读少量字段,代码最短

Basic flow

Create a parser, register start/end/text callbacks, feed data, free it. There is no result object — everything happens in callbacks.

PHP
<?php
$xml = '<?xml version="1.0" encoding="UTF-8"?>
<books>
  <book id="1"><title>PHP 入门</title></book>
  <book id="2"><title>PHP 进阶</title></book>
</books>';

$parser = xml_parser_create("UTF-8");
$depth  = 0;

xml_set_element_handler(
    $parser,
    function ($p, $name, $attrs) use (&$depth) {
        echo str_repeat("  ", $depth) . "开始: $name"
           . ($attrs ? " " . json_encode($attrs) : "") . "\n";
        $depth++;
    },
    function ($p, $name) use (&$depth) {
        $depth--;
        echo str_repeat("  ", $depth) . "结束: $name\n";
    }
);

xml_set_character_data_handler($parser, function ($p, $data) {
    $data = trim($data);
    if ($data !== "") {
        echo "文本: $data\n";
    }
});

xml_parse($parser, $xml, true);
xml_parser_free($parser);
Output
开始: BOOKS
  开始: BOOK {"ID":"1"}
    开始: TITLE
文本: PHP 入门
    结束: TITLE
  结束: BOOK
  开始: BOOK {"ID":"2"}
    开始: TITLE
文本: PHP 进阶
    结束: TITLE
  结束: BOOK
结束: BOOKS
Tag and attribute names are uppercased — Expat folds case by default. Disable with the XML_OPTION_CASE_FOLDING => 0 option array.

Preserving case

PHP
<?php
$xml = '<root><Item id="1">A</Item></root>';

$parser = xml_parser_create("UTF-8");
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);

xml_set_element_handler(
    $parser,
    function ($p, $name, $attrs) {
        echo "元素: $name" . ($attrs ? " " . json_encode($attrs) : "") . "\n";
    },
    function ($p, $name) {}
);

xml_parse($parser, $xml, true);
xml_parser_free($parser);
Output
元素: root
元素: Item {"id":"1"}

Collecting into arrays

In practice you accumulate: push on start, append text to the top, pop on end and store.

PHP
<?php
$xml = '<?xml version="1.0" encoding="UTF-8"?>
<books>
  <book id="1"><title>PHP 入门</title><price>59</price></book>
  <book id="2"><title>PHP 进阶</title><price>79</price></book>
</books>';

$books   = [];
$current = null;
$field   = null;

$p = xml_parser_create("UTF-8");

xml_set_element_handler(
    $p,
    function ($p, $name, $attrs) use (&$books, &$current, &$field) {
        if ($name === "BOOK") {
            $current = ["id" => $attrs["ID"] ?? "", "title" => "", "price" => 0];
        } elseif ($name === "TITLE" || $name === "PRICE") {
            $field = strtolower($name);
        }
    },
    function ($p, $name) use (&$books, &$current, &$field) {
        if ($name === "BOOK" && $current !== null) {
            $books[] = $current;
            $current = null;
        } elseif ($name === "TITLE" || $name === "PRICE") {
            $field = null;
        }
    }
);

xml_set_character_data_handler($p, function ($p, $data) use (&$current, &$field) {
    if ($current === null || $field === null) {
        return;
    }
    $current[$field] .= trim($data);
});

xml_parse($p, $xml, true);
xml_parser_free($p);

foreach ($books as $b) {
    echo "{$b['id']} {$b['title']} {$b['price']}\n";
}
echo "共 " . count($books) . " 本\n";
Output
1 PHP 入门 59
2 PHP 进阶 79
共 2 本
The text callback may fire multiple times for one string when it spans a buffer boundary. Always append with .=, never assign — the classic SAX bug.

Large files and errors

PHP
<?php
$path = __DIR__ . "/big.xml";
$fp = fopen($path, "r");

$p = xml_parser_create("UTF-8");
$count = 0;

xml_set_element_handler(
    $p,
    function ($p, $name) use (&$count) {
        if ($name === "BOOK") {
            $count++;
        }
    },
    function ($p, $name) {}
);

while (($chunk = fread($fp, 8192)) !== "") {
    if (!xml_parse($p, $chunk, feof($fp))) {
        $err = xml_get_error_code($p);
        echo "解析失败: " . xml_error_string($err) . " 行 " . xml_get_current_line_number($p) . "\n";
        break;
    }
}

fclose($fp);
xml_parser_free($p);

echo "读到 $count 个 book 节点\n";
Output
读到 2 个 book 节点

The third argument is_final must be true on the last chunk, or the parser never sees the document end.

Encoding issues

PHP
<?php
$xml = '<?xml version="1.0" encoding="GBK"?><r><t>中文</t></r>';

$p = xml_parser_create("UTF-8");
$text = "";

xml_set_character_data_handler($p, function ($p, $d) use (&$text) {
    $text .= $d;
});

// 让解析器按文档声明的编码处理
if (!xml_parse($p, $xml, true)) {
    echo "失败: " . xml_error_string(xml_get_error_code($p)) . "\n";
}
xml_parser_free($p);

echo $text, "\n";
Output
中文
xml_parser_create("UTF-8") sets the output encoding; input encoding comes from the XML declaration. A mismatch yields Encoding specified in XML declaration is incorrect.

Friends & Partners