XML Expat Parser
Event-driven SAX parsing: constant memory, ideal for large files.
Key PointsExpat fires callbacks as it reads without building a tree, so even tens of MB stay in memory.
Three approaches
| Approach | Model | Memory | Best 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
结束: BOOKSTag 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.三种解析方式
| 方式 | 模型 | 内存 | 适合 |
|---|---|---|---|
| Expat (SAX) | 事件回调,流式 | 恒定 | 大文件、只要提取部分数据 |
| DOM | 整棵树 | 与文件大小成正比 | 要改结构、要 XPath |
| SimpleXML | 对象映射 | 与文件大小成正比 | 读少量字段,代码最短 |
基本流程
创建解析器 → 注册开始/结束/文本回调 → 喂数据 → 释放。没有「解析结果」对象,一切都在回调里处理。
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标签名和属性名都变成了大写——Expat 默认开启大小写折叠。要保留原样,创建时传
XML_OPTION_CASE_FOLDING => 0 的选项数组。保留原始大小写
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"}提取数据到数组
实际用法是边解析边攒结果:用一个「当前元素」的栈,开始标签入栈,文本追加到栈顶,结束标签出栈并落库。
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 本文本回调可能被分多次调用——一段长文本在缓冲区边界会被切开。所以必须用
.= 追加,不能直接赋值。这是 SAX 解析最经典的 bug。大文件与错误处理
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 节点xml_parse 的第三个参数 is_final 必须在最后一块传 true,否则解析器不知道文档结束了,可能不触发收尾逻辑。
编码问题
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") 指定的是输出编码,输入编码由 XML 声明决定。声明 GBK 而实际是 UTF-8 字节时,会报 Encoding specified in XML declaration is incorrect。