PHP 內建三種 XML 解析器,各有優劣。本文深度對比 **DOMDocument (樹狀)**、**SimpleXML (易用)**、**XMLReader (流式/SAX)**,並提供生產級封裝建議。
| 維度 | DOMDocument | SimpleXML | XMLReader |
|---|---|---|---|
| 解析模式 | DOM 樹 (全量載入記憶體) | DOM 樹 (全量載入) | 流式拉取 (SAX-like,逐節點) |
| 記憶體佔用 | 高 (文件大小 5-10x) | 高 | 極低 (常數級) |
| 操作風格 | W3C DOM 標準 API | 屬性訪問語法糖 | 游標迭代 |
| 寫入/修改 | 完整支援 | 有限支援 | 不可 (唯讀) |
| XPath 支援 | 原生 DOMXPath | xpath() 方法 | 無 (需配合 DOM) |
| 命名空間 | 完整支援 | 需 registerXPathNamespace | 原生支援 |
| 適用場景 | 複雜修改、XPath 查詢、文檔構建 | 讀取已知結構、配置文件、小檔案 | 大檔案 (GB 級)、高併發、低記憶體 |
XML; $dom = new DOMDocument(); $dom->loadXML($xml); $dom->preserveWhiteSpace = false; $dom->formatOutput = true; $xpath = new DOMXPath($dom); // 1. 查詢所有 PHP 類書籍 $books = $xpath->query('//book[@category="PHP"]'); foreach ($books as $book) { echo "ID: " . $book->getAttribute('id') . "\n"; echo "標題: " . $xpath->evaluate('string(title)', $book) . "\n"; echo "價格: " . $xpath->evaluate('number(price)', $book) . "\n"; } // 2. 聚合查詢:平均價格 $avgPrice = $xpath->evaluate('number(//book/price)'); echo "平均價格: $avgPrice\n"; PHP 8 實戰 張三 580 2023-03-15 Python 數據科學 李四 620 2023-05-20
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$catalog = $dom->createElement('catalog');
$dom->appendChild($catalog);
$data = [
['id' => 'bk103', 'cat' => 'Go', 'title' => 'Go 微服務', 'author' => '王五', 'price' => 680],
];
foreach ($data as $item) {
$book = $dom->createElement('book');
$book->setAttribute('id', $item['id']);
$book->setAttribute('category', $item['cat']);
foreach (['title', 'author', 'price', 'publish_date'] as $field) {
$elem = $dom->createElement($field, $item[$field] ?? date('Y-m-d'));
$book->appendChild($elem);
}
$catalog->appendChild($book);
}
// 儲存
$dom->save('new_catalog.xml');
// 或輸出字串
$xmlString = $dom->saveXML();
$xml = simplexml_load_string($xmlString); // 或 simplexml_load_file('file.xml')
// 直接屬性訪問
echo $xml->book[0]->title; // "PHP 8 實戰"
echo $xml->book[0]['id']; // "bk101" (屬性)
// 迭代
foreach ($xml->book as $book) {
echo (string)$book->title . " - " . (string)$book->author . "\n";
// 強制轉型 (string) 獲取文本內容
}
// XPath 查詢
$phpBooks = $xml->xpath('//book[@category="PHP"]');
foreach ($phpBooks as $book) {
echo $book->title . "\n";
}
// 修改 (有限)
$xml->book[0]->price = 590;
$xml->book[0]->addChild('isbn', '978-7-111-12345-6');
// 命名空間處理
$xml->registerXPathNamespace('ns', 'https://example.com/ns');
$items = $xml->xpath('//ns:item');
open('huge_catalog.xml'); // 或 $reader->XML($hugeString);
$books = [];
while ($reader->read()) {
// 只關心 元素開始
if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'book') {
// 展開當前節點為 DOMNode (可選,需記憶體)
$domNode = $reader->expand();
$dom = new DOMDocument();
$bookNode = $dom->importNode($domNode, true);
$dom->appendChild($bookNode);
$xpath = new DOMXPath($dom);
$books[] = [
'id' => $xpath->evaluate('string(/book/@id)'),
'category' => $xpath->evaluate('string(/book/@category)'),
'title' => $xpath->evaluate('string(/book/title)'),
'price' => (float)$xpath->evaluate('number(/book/price)'),
];
// 釋放展開的 DOM 節點
$domNode = null;
$dom = null;
}
}
// 處理 $books ...
echo "共解析 " . count($books) . " 本書\n";
interface XmlParserInterface {
public function parse(string $source, callable $handler): void; // source: file path or XML string
public function query(string $xpath): iterable;
}
class DomXmlParser implements XmlParserInterface {
private DOMDocument $dom;
private DOMXPath $xpath;
public function __construct(private bool $preserveWhiteSpace = false) {}
public function parse(string $source, callable $handler): void {
$this->dom = new DOMDocument();
$this->dom->preserveWhiteSpace = $this->preserveWhiteSpace;
if (is_file($source)) {
$this->dom->load($source);
} else {
$this->dom->loadXML($source);
}
$this->xpath = new DOMXPath($this->dom);
$handler($this);
}
public function query(string $xpath): iterable {
return $this->xpath->query($xpath);
}
public function evaluate(string $xpath): mixed {
return $this->xpath->evaluate($xpath);
}
}
class StreamingXmlParser implements XmlParserInterface {
public function parse(string $source, callable $handler): void {
$reader = new XMLReader();
$reader->open($source);
while ($reader->read()) {
if ($reader->nodeType === XMLReader::ELEMENT) {
$handler($reader); // 傳遞 reader 讓調用方決定是否 expand()
}
}
$reader->close();
}
public function query(string $xpath): iterable {
throw new \BadMethodCallException('Streaming parser does not support XPath query');
}
}
// 使用
$parser = new DomXmlParser();
$parser->parse('catalog.xml', function($p) {
foreach ($p->query('//book[@category="PHP"]') as $book) {
echo $book->getAttribute('id') . ': ' . $p->evaluate('string(title)', $book) . "\n";
}
});
| 問題 | 原因 | 解決 |
|---|---|---|
| 載入失敗/亂碼 | XML 宣告編碼與實際不符、含 BOM、非法字元 | `$dom->loadXML(mb_convert_encoding($xml, 'UTF-8', 'auto'))`、移除 BOM、過濾控制字元 |
| SimpleXML 丟失屬性/命名空間 | 轉 JSON 時 `json_encode` 不處理屬性 | 手動遞歸轉陣列、或用 DOMDocument |
| XMLReader expand() 記憶體暴增 | 大量節點展開為 DOM | 僅展開必要節點、用完即釋放、或改用純游標讀取屬性 |
| XPath 查無結果 | 命名空間未註冊、大小寫不匹配 | `$xpath->registerNamespace('pfx', 'uri')`、確認節點名稱大小寫 |
| 實體引用錯誤 | XML 含未定義實體 `©` 等 | `$dom->substituteEntities = true`、或預先定義 DTD |
**DOMDocument** 為通用首選 (功能全、標準化);**SimpleXML** 適配簡單讀取;**XMLReader** 專治大檔案。封裝統一介面,根據文件大小動態選擇解析器,是架構層面的最佳實踐。