Python3 DOM 操作与 xml.dom.minidom

🎉摘要:本文详细讲解Python中xml.dom.minidom模块的使用,涵盖DOM核心概念、XML文件与字符串解析、节点遍历与修改、节点类型处理、美化输出,并与ElementTree进行对比。最后通过一个实战的XML配置文件编辑器示例,展示minidom在增删改查和持久化方面的应用。

DOM(Document Object Model)是 W3C 定义的 XML 处理标准。Python 的 xml.dom.minidom 提供了轻量级实现。虽然 ElementTree 更简洁,但了解 DOM 有助于理解底层 XML 结构,并在需要标准兼容时派上用场。

如果你有前端经验(使用 JavaScript 操作 HTML 的 DOM 结构),学习后续内容将事半功倍。

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>

注意,后续示例代码将使用该文件。

解析 XML

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_NODE1元素节点
ATTRIBUTE_NODE2属性节点
TEXT_NODE3文本节点
COMMENT_NODE8注释节点
DOCUMENT_NODE9文档节点

下面通过一个示例来演示各种节点类型,依然通过 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 对比

下面从多个角度对比两者的差异,可根据需要进行选择:

特性minidomElementTree
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 文档。

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号