DOM 操作
PHP 的 DOM 扩展实现了 W3C DOM 标准,提供了操作 XML 和 HTML 文档的完整 API。PHP 8.4 引入了现代化的 DOM 命名空间,使 API 更加面向对象和类型安全。本节将全面讲解 PHP DOM 操作的核心方法。
前置知识
阅读本节前,建议先了解:PHP 面向对象编程、字符串处理
基础概念
什么是 DOM
DOM(Document Object Model,文档对象模型)将 XML/HTML 文档表示为树形结构,其中每个节点都是一个对象。PHP 的 DOM 扩展基于 libxml2 库,完全遵循 W3C DOM Level 2/3 标准。
DOM 树结构
Document
|-- Element (html)
|-- Element (head)
| |-- Element (title)
| |-- Text ("页面标题")
|-- Element (body)
|-- Element (div)
|-- Attribute (class="container")
|-- Text ("内容")PHP 8.4 DOM 命名空间
PHP 8.4 将 DOM 类移至 DOM\ 命名空间下,提供了更好的类型安全和现代化 API:
php
<?php
declare(strict_types=1);
// PHP 8.4+ 新命名空间
$doc = new DOM\HTMLDocument();
$doc = new DOM\XMLDocument();
$element = $doc->createElement('div');
// 旧 API 仍可用(向后兼容)
$doc = new DOMDocument();创建 XML 文档
从字符串加载
php
<?php
declare(strict_types=1);
$xmlString = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book category="fiction">
<title lang="en">Harry Potter</title>
<author>J.K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="programming">
<title lang="en">Clean Code</title>
<author>Robert C. Martin</author>
<year>2008</year>
<price>39.99</price>
</book>
</bookstore>
XML;
$doc = new DOMDocument();
$doc->loadXML($xmlString, LIBXML_NOBLANKS);
echo $doc->saveXML();从文件加载
php
<?php
declare(strict_types=1);
$doc = new DOMDocument();
$doc->preserveWhiteSpace = false; // 去除空白节点
$doc->formatOutput = true; // 格式化输出
$doc->load('books.xml', LIBXML_NOBLANKS);
echo "文档编码: " . $doc->encoding . PHP_EOL;
echo "XML 版本: " . $doc->xmlVersion . PHP_EOL;
echo "是否独立: " . ($doc->xmlStandalone ? 'yes' : 'no') . PHP_EOL;创建新文档
php
<?php
declare(strict_types=1);
$doc = new DOMDocument('1.0', 'UTF-8');
$doc->formatOutput = true;
// 创建根元素
$root = $doc->createElement('configuration');
$doc->appendChild($root);
// 创建子元素
$database = $doc->createElement('database');
$root->appendChild($database);
$host = $doc->createElement('host', 'localhost');
$database->appendChild($host);
$port = $doc->createElement('port', '3306');
$database->appendChild($port);
$name = $doc->createElement('name', 'myapp');
$database->appendChild($name);
// 添加注释
$comment = $doc->createComment('数据库配置');
$root->insertBefore($comment, $database);
// 添加 CDATA
$cdata = $doc->createCDATASection('<strong>重要数据</strong>');
$note = $doc->createElement('note');
$note->appendChild($cdata);
$root->appendChild($note);
echo $doc->saveXML();查询元素
getElementById
php
<?php
declare(strict_types=1);
$doc = new DOMDocument();
$doc->loadXML($xmlString);
$doc->validate(); // 需要 DTD 验证 ID 属性
$element = $doc->getElementById('book1');
if ($element) {
echo $element->nodeValue;
}getElementsByTagName
php
<?php
declare(strict_types=1);
$doc = new DOMDocument();
$doc->loadXML($xmlString);
$books = $doc->getElementsByTagName('book');
foreach ($books as $book) {
$title = $book->getElementsByTagName('title')->item(0);
$category = $book->getAttribute('category');
echo "{$category}: {$title->nodeValue}" . PHP_EOL;
}getElementsByTagNameNS
php
<?php
declare(strict_types=1);
// 带命名空间的 XML
$nsXml = <<<XML
<?xml version="1.0"?>
<root xmlns:ns1="http://example.com/ns1" xmlns:ns2="http://example.com/ns2">
<ns1:item id="1">命名空间元素1</ns1:item>
<ns2:item id="2">命名空间元素2</ns2:item>
</root>
XML;
$doc = new DOMDocument();
$doc->loadXML($nsXml);
// 获取特定命名空间的元素
$ns1Items = $doc->getElementsByTagNameNS('http://example.com/ns1', 'item');
foreach ($ns1Items as $item) {
echo $item->nodeValue . PHP_EOL; // 命名空间元素1
}XPath 查询
php
<?php
declare(strict_types=1);
$doc = new DOMDocument();
$doc->loadXML($xmlString);
$xpath = new DOMXPath($doc);
// 基本查询
$titles = $xpath->query('/bookstore/book/title');
foreach ($titles as $title) {
echo $title->nodeValue . PHP_EOL;
}
// 条件查询
$expensiveBooks = $xpath->query('/bookstore/book[price > 30]/title');
foreach ($expensiveBooks as $title) {
echo "高价书: {$title->nodeValue}" . PHP_EOL;
}
// 属性查询
$fictionBooks = $xpath->query('/bookstore/book[@category="fiction"]');
foreach ($fictionBooks as $book) {
$title = $xpath->query('title', $book)->item(0);
echo "小说: {$title->nodeValue}" . PHP_EOL;
}
// 统计
$count = $xpath->evaluate('count(/bookstore/book)');
echo "书籍总数: {$count}" . PHP_EOL;
// 包含文本匹配
$books = $xpath->query('/bookstore/book[contains(title, "Code")]');XPath 命名空间
php
<?php
declare(strict_types=1);
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('ns1', 'http://example.com/ns1');
$xpath->registerNamespace('ns2', 'http://example.com/ns2');
// 查询特定命名空间的元素
$items = $xpath->query('//ns1:item');修改文档
添加和删除节点
php
<?php
declare(strict_types=1);
$doc = new DOMDocument();
$doc->loadXML($xmlString);
$doc->formatOutput = true;
// 添加新节点
$newBook = $doc->createElement('book');
$newBook->setAttribute('category', 'science');
$title = $doc->createElement('title', 'A Brief History of Time');
$author = $doc->createElement('author', 'Stephen Hawking');
$year = $doc->createElement('year', '1988');
$price = $doc->createElement('price', '25.00');
$newBook->appendChild($title);
$newBook->appendChild($author);
$newBook->appendChild($year);
$newBook->appendChild($price);
// 添加到根元素
$root = $doc->documentElement;
$root->appendChild($newBook);
// 插入到特定位置
$firstBook = $doc->getElementsByTagName('book')->item(0);
$root->insertBefore($newBook, $firstBook);
// 删除节点
$lastBook = $doc->getElementsByTagName('book')->item(
$doc->getElementsByTagName('book')->length - 1
);
$lastBook->parentNode->removeChild($lastBook);
// 替换节点
$oldPrice = $xpath->query('/bookstore/book[1]/price')->item(0);
$newPrice = $doc->createElement('price', '19.99');
$oldPrice->parentNode->replaceChild($newPrice, $oldPrice);
// 克隆节点
$clonedBook = $newBook->cloneNode(true); // true = 深度克隆
$root->appendChild($clonedBook);
echo $doc->saveXML();修改属性和文本
php
<?php
declare(strict_types=1);
$doc = new DOMDocument();
$doc->loadXML($xmlString);
// 修改属性
$book = $doc->getElementsByTagName('book')->item(0);
$book->setAttribute('category', 'fantasy');
// 获取和修改属性
$category = $book->getAttribute('category');
$book->removeAttribute('category');
// 判断属性是否存在
if ($book->hasAttribute('category')) {
echo $book->getAttribute('category');
}
// 修改文本内容
$title = $doc->getElementsByTagName('title')->item(0);
$title->nodeValue = '新标题';
// 修改带命名空间的属性
$book->setAttributeNS(
'http://www.w3.org/2000/xmlns/',
'xmlns:custom',
'http://example.com/custom'
);
echo $doc->saveXML();遍历 DOM 树
基本遍历
php
<?php
declare(strict_types=1);
function traverseDom(DOMNode $node, int $depth = 0): void
{
$indent = str_repeat(' ', $depth);
switch ($node->nodeType) {
case XML_ELEMENT_NODE:
$attrs = '';
if ($node->hasAttributes()) {
foreach ($node->attributes as $attr) {
$attrs .= " {$attr->nodeName}=\"{$attr->nodeValue}\"";
}
}
echo "{$indent}<{$node->nodeName}{$attrs}>" . PHP_EOL;
break;
case XML_TEXT_NODE:
$text = trim($node->nodeValue);
if ($text !== '') {
echo "{$indent}Text: {$text}" . PHP_EOL;
}
break;
case XML_COMMENT_NODE:
echo "{$indent}Comment: {$node->nodeValue}" . PHP_EOL;
break;
}
if ($node->hasChildNodes()) {
foreach ($node->childNodes as $child) {
traverseDom($child, $depth + 1);
}
}
}
$doc = new DOMDocument();
$doc->loadXML($xmlString);
traverseDom($doc->documentElement);PHP 8.4 DOM 命名空间新 API
php
<?php
declare(strict_types=1);
// PHP 8.4+ 使用新的 DOM\ 命名空间
use DOM\HTMLDocument;
use DOM\XMLDocument;
use DOM\Element;
use DOM\NodeList;
$doc = XMLDocument::createFromString($xmlString);
// 改进的 API
$books = $doc->getElementsByTagName('book');
// 遍历改进
foreach ($books as $book) {
// $book 类型为 Element(而不是 DOMElement)
echo $book->getAttribute('category') . PHP_EOL;
}
// 新增方法
$children = $doc->getElementById('book1');
// ...HTML 处理
加载 HTML
php
<?php
declare(strict_types=1);
$htmlString = <<<HTML
<!DOCTYPE html>
<html>
<head><title>测试页面</title></head>
<body>
<div class="container">
<h1>标题</h1>
<p class="content">段落内容</p>
<a href="https://example.com">链接</a>
<ul>
<li>项目 1</li>
<li>项目 2</li>
<li>项目 3</li>
</ul>
</div>
</body>
</html>
HTML;
$doc = new DOMDocument();
// 抑制 HTML 解析时的警告(不严格的 HTML 不会报错)
@$doc->loadHTML($htmlString, LIBXML_NOERROR | LIBXML_NOWARNING);
// 查询元素
$xpath = new DOMXPath($doc);
// 获取所有链接
$links = $xpath->query('//a');
foreach ($links as $link) {
echo $link->getAttribute('href') . ": " . trim($link->textContent) . PHP_EOL;
}
// 获取列表项
$items = $xpath->query('//ul/li');
foreach ($items as $item) {
echo "- " . trim($item->textContent) . PHP_EOL;
}HTML 输出
php
<?php
declare(strict_types=1);
$doc = new DOMDocument('1.0', 'UTF-8');
$doc->loadHTML($htmlString);
// 保存为 HTML
$html = $doc->saveHTML();
// 保存特定节点
$body = $doc->getElementsByTagName('body')->item(0);
$bodyHtml = $doc->saveHTML($body);
// 保存为 XML(XHTML)
$xhtml = $doc->saveXML($doc->documentElement);实战示例
XML 配置读写
php
<?php
declare(strict_types=1);
/**
* XML 配置管理器
*/
class XmlConfigManager
{
public function __construct(
private readonly string $filePath
) {}
public function get(string $key, mixed $default = null): mixed
{
$doc = new DOMDocument();
$doc->load($this->filePath);
$xpath = new DOMXPath($doc);
$nodes = $xpath->query("//setting[@name='{$key}']/value");
if ($nodes->length > 0) {
return $nodes->item(0)->nodeValue;
}
return $default;
}
public function set(string $key, string $value): void
{
$doc = new DOMDocument();
$doc->formatOutput = true;
$doc->load($this->filePath);
$xpath = new DOMXPath($doc);
$existing = $xpath->query("//setting[@name='{$key}']");
if ($existing->length > 0) {
// 更新
$valueNode = $xpath->query("//setting[@name='{$key}']/value")->item(0);
$valueNode->nodeValue = $value;
} else {
// 新增
$root = $doc->documentElement;
$setting = $doc->createElement('setting');
$setting->setAttribute('name', $key);
$valueNode = $doc->createElement('value', $value);
$setting->appendChild($valueNode);
$root->appendChild($setting);
}
$doc->save($this->filePath);
}
public function getAll(): array
{
$doc = new DOMDocument();
$doc->load($this->filePath);
$xpath = new DOMXPath($doc);
$settings = $xpath->query('//setting');
$result = [];
foreach ($settings as $setting) {
$key = $setting->getAttribute('name');
$value = $xpath->query('value', $setting)->item(0)->nodeValue;
$result[$key] = $value;
}
return $result;
}
public function remove(string $key): void
{
$doc = new DOMDocument();
$doc->load($this->filePath);
$xpath = new DOMXPath($doc);
$setting = $xpath->query("//setting[@name='{$key}']")->item(0);
if ($setting) {
$setting->parentNode->removeChild($setting);
$doc->save($this->filePath);
}
}
}注意事项
LIBXML 选项
php
<?php
declare(strict_types=1);
// 常用选项
LIBXML_NOBLANKS // 去除空白文本节点
LIBXML_NOENT // 替换实体引用
LIBXML_NOERROR // 抑制错误报告
LIBXML_NOWARNING // 抑制警告报告
LIBXML_NONET // 禁止网络访问
LIBXML_NOCDATA // 合并 CDATA 为文本节点
LIBXML_COMPACT // 小节点分配优化
LIBXML_PARSE_HUGE // 放松解析器限制
// 组合使用
$doc->loadXML($xml, LIBXML_NOBLANKS | LIBXML_NOERROR);最佳实践
- 使用 XPath:对于复杂查询,XPath 比遍历更高效
- 设置 formatOutput:调试时启用格式化输出
- 使用 LIBXML_NOBLANKS:避免空白节点干扰遍历
- PHP 8.4+ 使用新 API:
DOM\XMLDocument提供更好的类型安全 - 大文件用 XMLReader:对于超大 XML 文件,使用 XMLReader 而非 DOM
- XSS 防护:处理 HTML 时使用
htmlspecialchars转义用户输入
下一节
继续学习:SimpleXML