DOM(Document Object Model)是 W3C 定义的 XML 处理标准。Python 的 xml.dom.minidom 提供了轻量级实现。虽然 ElementTree 更简洁,但了解 DOM 有助于理解底层 XML 结构,并在需要标准兼容时派上用场。
如果你有前端经验(使用 JavaScript 操作 HTML 的 DOM 结构),学习后续内容将事半功倍。
DOM 将 XML 文档视为树形结构,每个节点都是对象:
Document:整个文档的根节点
Element:元素节点(标签)
Text:文本内容节点
Attribute:属性节点
Comment:注释节点
<?xml version="1.0"?>
<message id="1">
<to>张三</to>
<content>你好</content>
</message>DOM 视角下的结构:
Document
└── Element(message, id="1")
├── Element(to)
│ └── Text("张三")
└── Element(content)
└── Text("你好")在正式解析之前,我们假设当前脚本所在目录下面存在 sample.xml 文件,内容如下:
<?xml version="1.0"?>
<message id="1">
<to>张三</to>
<content>你好</content>
</message>注意,后续示例代码将使用该文件。
minidom 解析 XML 文件有两种方式,分别如下:
(1)从文件解析:即指定一个文件,minidom 库自动读取文件内容进行解析,如下:
from xml.dom import minidom
# 从文件解析,这里采用的相对路径,也可以使用绝对路径哦
doc = minidom.parse('sample.xml')
# 获取根元素
root = doc.documentElement
print(root.tagName) # message(2)从字符串解析:这种方式在通过接口通信的场景下面使用频率很高,直接将接口收到的 XML 字符串进行解析,不用存储到临时文件,再来解析。例如:
from xml.dom import minidom
# 从字符串解析
xml_string = '<?xml version="1.0"?><message id="1"><to>张三</to><content>你好</content></message>'
doc = minidom.parseString(xml_string)
# 获取根元素
root = doc.documentElement
print(root.tagName) # message遍历即将 XML 文档从头到尾逐一读取分析,找到我们需要的标签和内容。例如,下面将遍历所有 book 标签,并且获取它的属性和子元素,最后在控制台输出遍历信息。
from xml.dom import minidom
xml_data = '''
<library>
<book genre="fiction">
<title>三体</title>
<author>刘慈欣</author>
</book>
<book genre="science">
<title>时间简史</title>
<author>霍金</author>
</book>
</library>
'''
doc = minidom.parseString(xml_data)
root = doc.documentElement
# 获取所有子元素(仅 Element 节点)
books = root.getElementsByTagName('book')
print(f"共有 {len(books)} 本书")
for book in books:
# 获取属性
genre = book.getAttribute('genre')
# 获取子元素内容
title = book.getElementsByTagName('title')[0]
author = book.getElementsByTagName('author')[0]
# 获取文本内容
title_text = title.firstChild.data if title.firstChild else ''
author_text = author.firstChild.data if author.firstChild else ''
print(f"[{genre}] {title_text} - {author_text}")运行例子输出:
共有 2 本书
[fiction] 三体 - 刘慈欣
[science] 时间简史 - 霍金注意,如果熟悉 JavaScript 知识,getElementsByTagName 是不是非常眼熟,这不就是根据标签名称获取标签列表的功能吗。这就是为什么 minidom 后面有 dom 字眼吧!
注意,在 DOM 中,元素内的文本是一个独立的 Text 节点。下面演示遍历指定元素的所有子元素,然后通过 nodeType 属性判断节点的类型,根据不同的节点类型输出不同的文本信息,代码如下:
from xml.dom import minidom
xml_data = '<greeting>Hello <b>World</b>!</greeting>'
doc = minidom.parseString(xml_data)
element = doc.documentElement
# 遍历所有子节点
for node in element.childNodes:
if node.nodeType == node.TEXT_NODE:
print(f"文本节点: '{node.data}'")
elif node.nodeType == node.ELEMENT_NODE:
print(f"元素节点: <{node.tagName}>")运行示例输出:
文本节点: 'Hello '
元素节点: <b>
文本节点: '!'注意,node.TEXT_NODE 表示是 TEXT 节点,node.ELEMENT_NODE 表示是元素节点。
除了上面见过的 TEXT 节点和元素节点,还有属性、注释、文档节点,所有节点类型见下表:
| 常量 | 值 | 说明 |
| ELEMENT_NODE | 1 | 元素节点 |
| ATTRIBUTE_NODE | 2 | 属性节点 |
| TEXT_NODE | 3 | 文本节点 |
| COMMENT_NODE | 8 | 注释节点 |
| DOCUMENT_NODE | 9 | 文档节点 |
下面通过一个示例来演示各种节点类型,依然通过 nodeType 属性进行判断,代码如下:
from xml.dom import minidom
def analyze_node(node, depth=0):
"""递归分析节点结构"""
indent = " " * depth
if node.nodeType == node.ELEMENT_NODE:
print(f"{indent}元素: <{node.tagName}>")
# 打印属性
if node.attributes:
for attr in node.attributes.values():
print(f"{indent} @{attr.name} = {attr.value}")
elif node.nodeType == node.TEXT_NODE:
text = node.data.strip()
if text:
print(f"{indent}文本: \"{text}\"")
elif node.nodeType == node.COMMENT_NODE:
print(f"{indent}注释: <!--{node.data}-->")
# 递归处理子节点
if hasattr(node, 'childNodes'):
for child in node.childNodes:
analyze_node(child, depth + 1)
# 使用示例
xml_data = '''
<!-- 产品列表 -->
<products version="1.0">
<product id="p1">手机</product>
</products>
'''
doc = minidom.parseString(xml_data)
analyze_node(doc.documentElement)运行示例输出如下:
元素: <products>
@version = 1.0
元素: <product>
@id = p1
文本: "手机"修改文档指对已经存在的 XML 文档添加元素、修改已有的元素或者删除某个节点。这和前端开发中使用 JavaScript 对 HTML DOM 操作类似。
使用 createElement(新元素名称) 方法创建一个元素,元素创建后可以使用 setAttribute("属性名", "属性值") 方法为它添加多个属性,最后通过 appendChild(新元素) 方法将新创建的元素放在某个元素下面。例如:
from xml.dom import minidom
# 创建空文档
doc = minidom.Document()
# 创建根元素
root = doc.createElement('config')
doc.appendChild(root)
# 创建带属性的元素
setting = doc.createElement('setting')
setting.setAttribute('name', 'theme')
setting.setAttribute('value', 'dark')
root.appendChild(setting)
# 创建带文本的元素
timeout = doc.createElement('timeout')
text_node = doc.createTextNode('30')
timeout.appendChild(text_node)
root.appendChild(timeout)
print(doc.toprettyxml(indent=' '))运行示例输出:
<?xml version="1.0" ?>
<config>
<setting name="theme" value="dark"/>
<timeout>30</timeout>
</config>注意,上面示例中的文档并没有持久化到文件,是存储到内容中的。
在 minidom 中修改元素与 HTML 中使用 JavaScript 修改元素存在一些差异,在 HTML 中,通过 JavaScript 根据 class、id、标签名找到要修改的元素,然后调用元素上面的方法对 DOM 进行修改。而在 minidom 中,是创建一个新元素,然后通过 replaceChild(新元素,旧元素) 去替换旧元素。代码如下:
from xml.dom import minidom
xml_data = '<item price="10">苹果</item>'
doc = minidom.parseString(xml_data)
item = doc.documentElement
# 修改属性
item.setAttribute('price', '15')
# 修改文本内容(需要替换文本节点)
new_text = doc.createTextNode('红苹果')
item.replaceChild(new_text, item.firstChild)
print(item.toxml()) # <item price="15">红苹果</item>注意,代码中的 item.firstChild 将返回 item 的第一个直接子节点,包含文本节点、注释、元素节点,不只是标签。
删除节点指将某个节点从 XML 文档树中移除,在 minidom 中,通过 removeChild(子节点) 方法删除指定节点的子节点,注意,这里一定要是有效的直接子节点。例如:
from xml.dom import minidom
xml_data = '''
<todo_list>
<task status="done">任务1</task>
<task status="pending">任务2</task>
<task status="done">任务3</task>
</todo_list>
'''
doc = minidom.parseString(xml_data)
root = doc.documentElement
# 删除所有已完成的任务
tasks = root.getElementsByTagName('task')
for task in list(tasks): # 转成 list 避免遍历时修改的问题
if task.getAttribute('status') == 'done':
root.removeChild(task)
task.unlink() # 释放内存
print(root.toxml())运行实例,输出如下:
<todo_list>
<task status="pending">任务2</task>
</todo_list>到这里,前面已经介绍了使用 minidom 对 XML 文档进行增删改查,学会这些,操作 XML 文档完全不在话下,下面将介绍在将 XML 输出到 XML 文件时,如何保证输出的 XML 格式良好,方便人阅读,而不是压缩成一行,阅读极不方便。例如:
from xml.dom import minidom
def pretty_print(xml_string):
"""美化 XML 字符串输出"""
doc = minidom.parseString(xml_string)
return doc.toprettyxml(indent=' ')
# 使用示例
ugly_xml = '<root><item id="1"><name>测试</name></item></root>'
print(pretty_print(ugly_xml))上面代码中,toprettyxml() 是 minidom 文档对象(doc)自带方法,作用是格式化输出 XML 字符串。indent=' ' 参数指定缩进字符,这里是两个空格,子节点会自动缩进,让 XML 排版工整可读。如果不调用该方法直接打印只会输出紧凑无换行的单行 XML。调用了该方法它会自动换行 + 分层缩进(indent),生成美观格式化文本。
运行示例,输出如下:
<?xml version="1.0" ?>
<root>
<item id="1">
<name>测试</name>
</item>
</root>自定义格式化,剔除 XML 文档中的空格行,例如:
from xml.dom import minidom
def to_formatted_xml(doc, indent=' '):
"""自定义格式化,去除空行"""
lines = []
for line in doc.toprettyxml(indent=indent).split('\n'):
# 移除每行前后的空格,非空才保留
# 实现移除空白行
if line.strip():
lines.append(line)
return '\n'.join(lines)
# 使用示例
xml_string = '''<root>
<item id="1">
<name>测试</name>
</item></root>'''
doc = minidom.parseString(xml_string)
print(to_formatted_xml(doc))运行示例,输出如下:
<?xml version="1.0" ?>
<root>
<item id="1">
<name>测试</name>
</item>
</root>下面从多个角度对比两者的差异,可根据需要进行选择:
| 特性 | minidom | ElementTree |
| API 风格 | W3C 标准,熟悉 HTML DOC 的开发者更容易上手 | Pythonic |
| 代码量 | 较多 | 较少 |
| 功能完整 | 标准 DOM | 简化 API |
| 性能 | 较慢 | 较快 |
| 内存占用 | 较高 | 较低 |
| 适用场景 | 需标准兼容 | 一般用途 |
假如使用 minidom 和 ElementTree 对同一个 XML 进行相同的操作,比较一下它们之间代码的量和复杂度。如下:
(1)假如存在 data.xml 文件,内容如下:
<?xml version="1.0"?>
<users>
<user>
<name>张三</name>
<age>28</age>
</user>
<user>
<name>李四</name>
<age>24</age>
</user>
</users>(2)分别使用 minidom 和 ElementTree 遍历 user 信息,如下:
ElementTree 版本:
import xml.etree.ElementTree as ET
tree = ET.parse('data.xml')
root = tree.getroot()
for user in root.findall('user'):
name = user.find('name').text
age = user.find('age').text
print(f"{name}: {age}")minidom 版本:
from xml.dom import minidom
doc = minidom.parse('data.xml')
root = doc.documentElement
users = root.getElementsByTagName('user')
for user in users:
name = user.getElementsByTagName('name')[0].firstChild.data
age = user.getElementsByTagName('age')[0].firstChild.data
print(f"{name}: {age}")运行示例,输出内容一样:
张三: 28
李四: 24但是,代码看上去明显 ElementTree 的代码量要少,而且 ElementTree 的方法名称更简洁,同样是获取元素的文本内容,ElementTree 使用 text,而 minidom 使用 .firstChild.data,更繁琐。在生产环境,user.getElementsByTagName('name')[0] 这样写可能会出错,如果没有 name 节点,[0] 越界了。
该示例将使用 minidom 实现一个简单配置文件编辑器(DOMConfigEditor 类),该编辑器类支持对 XML 文档的查询、修改、删除,以及持久化到磁盘的功能。
完整代码如下:
from xml.dom import minidom
from typing import Optional
class DOMConfigEditor:
"""基于 DOM 的配置文件编辑器"""
def __init__(self, filepath: Optional[str] = None):
self.filepath = filepath
if filepath:
self.doc = minidom.parse(filepath)
else:
self.doc = minidom.Document()
self.doc.appendChild(self.doc.createElement('configuration'))
def get_value(self, section: str, key: str) -> str:
"""获取配置值"""
section_elem = self._get_or_create_section(section)
key_elem = section_elem.getElementsByTagName(key)
if key_elem and key_elem[0].firstChild:
return key_elem[0].firstChild.data
return ''
def set_value(self, section: str, key: str, value: str):
"""设置配置值"""
section_elem = self._get_or_create_section(section)
# 查找或创建 key 元素
key_elems = section_elem.getElementsByTagName(key)
if key_elems:
elem = key_elems[0]
else:
elem = self.doc.createElement(key)
section_elem.appendChild(elem)
# 设置文本内容
if elem.firstChild:
elem.firstChild.data = value
else:
text = self.doc.createTextNode(value)
elem.appendChild(text)
def _get_or_create_section(self, name: str):
"""获取或创建配置节"""
root = self.doc.documentElement
sections = root.getElementsByTagName(name)
if sections:
return sections[0]
section = self.doc.createElement(name)
root.appendChild(section)
return section
def remove_key(self, section: str, key: str):
"""删除配置项"""
section_elem = self._get_or_create_section(section)
key_elems = section_elem.getElementsByTagName(key)
for elem in key_elems:
section_elem.removeChild(elem)
elem.unlink()
def save(self, filepath: Optional[str] = None):
"""保存文档"""
path = filepath or self.filepath
if not path:
raise ValueError("未指定文件路径")
with open(path, 'w', encoding='utf-8') as f:
self.doc.writexml(f, indent=' ', addindent=' ', newl='\n')
def to_string(self) -> str:
"""返回格式化字符串"""
return self.doc.toprettyxml(indent=' ')
# 使用示例
if __name__ == '__main__':
import tempfile
import os
# 创建配置
editor = DOMConfigEditor()
editor.set_value('database', 'host', 'localhost')
editor.set_value('database', 'port', '5432')
editor.set_value('app', 'debug', 'true')
print("创建的配置:")
print(editor.to_string())
# 保存并重新加载
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
temp_path = f.name
editor.save(temp_path)
# 读取并修改
editor2 = DOMConfigEditor(temp_path)
print(f"原 host: {editor2.get_value('database', 'host')}")
editor2.set_value('database', 'host', '192.168.1.1')
print(f"新 host: {editor2.get_value('database', 'host')}")
os.unlink(temp_path)运行示例,输出如下:
创建的配置:
<?xml version="1.0" ?>
<configuration>
<database>
<host>localhost</host>
<port>5432</port>
</database>
<app>
<debug>true</debug>
</app>
</configuration>
原 host: localhost
新 host: 192.168.1.1下一节将介绍如何通过 SAX 事件驱动去解析 XML,这非常适合大的 XML 文档。