Python进阶教程:5_XML 解析 —— 新手完全指南

一、什么是 XML?

XML (eXtensible Markup Language,可扩展标记语言)是一种用来存储和传输数据的文本格式。

1.1 生活比喻

把 XML 想象成一个带标签的收纳盒

  • 每个盒子有名字(标签名)
  • 盒子里可以放东西(文本内容)
  • 盒子里还可以套小盒子(子元素)
  • 每个盒子可以贴便签(属性)

1.2 XML 长什么样?

python 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
    <book category="编程">
        <title>Python入门</title>
        <author>张三</author>
        <price>59.9</price>
        <year>2025</year>
    </book>
    <book category="文学">
        <title>红楼梦</title>
        <author>曹雪芹</author>
        <price>39.0</price>
        <year>1791</year>
    </book>
</bookstore>

1.3 XML 的基本结构

python 复制代码
<?xml version="1.0" encoding="UTF-8"?>   ← XML 声明(可选)
<bookstore>                               ← 根元素(必须有且只有一个)
    <book category="编程">                ← 子元素,category 是"属性"
        <title>Python入门</title>         ← 子元素,"Python入门"是"文本内容"
        <author>张三</author>
        <price>59.9</price>
    </book>
</bookstore>

1.4 XML 的术语对照表

python 复制代码
<person name="张三" age="25">    ← "person" 是元素(标签)
    <city>北京</city>            ← "name"、"age" 是属性
</person>                        ← "北京" 是文本内容
术语 英文 示例 说明
元素 Element <book>...</book> 一对标签包裹的内容
标签 Tag booktitle 尖括号里的名字
属性 Attribute category="编程" 写在开始标签里的键值对
文本 Text Python入门 标签之间的文字
根元素 Root <bookstore> 最外层的元素
子元素 Child <book><bookstore> 的子元素 嵌套关系
父元素 Parent <bookstore><book> 的父元素 上一级
兄弟元素 Sibling 两个 <book> 互为兄弟 同级

1.5 XML 的规则

python 复制代码
✅ 必须有且只有一个根元素
✅ 标签必须成对关闭:<title>...</title>
✅ 标签必须正确嵌套:<a><b></b></a>  ✅
                    <a><b></a></b>  ❌
✅ 属性值必须加引号:name="张三"  ✅
                    name=张三    ❌
✅ 标签名区分大小写:<Title> 和 <title> 是不同的

二、Python 解析 XML 的方式概览

Python 提供了多种解析 XML 的方式:

方式 模块 特点 适合场景
ElementTree xml.etree.ElementTree 简单、轻量、Pythonic 新手首选,日常使用
minidom xml.dom.minidom DOM方式,功能全 需要完整DOM操作
SAX xml.sax 事件驱动,流式解析 超大文件,内存敏感
lxml lxml(第三方) 功能最强,速度快 复杂XPath、性能要求高

本教程重点讲 ElementTree(最常用),其他方式也会介绍。


三、ElementTree ------ 最推荐的方式

3.1 导入模块

python 复制代码
import xml.etree.ElementTree as ET

约定俗成:用 ET 作为别名。

3.2 准备一个 XML 文件

创建文件 books.xml

python 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
    <book id="1" category="编程">
        <title>Python编程:从入门到实践</title>
        <author>Eric Matthes</author>
        <price currency="CNY">89.00</price>
        <year>2020</year>
        <tags>
            <tag>Python</tag>
            <tag>入门</tag>
        </tags>
    </book>
    <book id="2" category="文学">
        <title>百年孤独</title>
        <author>加西亚·马尔克斯</author>
        <price currency="CNY">55.00</price>
        <year>1967</year>
        <tags>
            <tag>小说</tag>
            <tag>魔幻现实主义</tag>
        </tags>
    </book>
    <book id="3" category="编程">
        <title>算法导论</title>
        <author>Thomas H. Cormen</author>
        <price currency="USD">128.00</price>
        <year>2009</year>
        <tags>
            <tag>算法</tag>
            <tag>计算机</tag>
        </tags>
    </book>
</bookstore>

3.3 解析 XML 文件(从文件读取)

python 复制代码
import xml.etree.ElementTree as ET

# ============ 第一步:解析文件,得到"树"对象 ============
tree = ET.parse("books.xml")

# ============ 第二步:获取根元素 ============
root = tree.getroot()

# 查看根元素的信息
print(f"根元素标签:{root.tag}")        # bookstore
print(f"根元素属性:{root.attrib}")     # {}(根元素没有属性)
print(f"子元素数量:{len(root)}")       # 3(有3个book)

输出

python 复制代码
根元素标签:bookstore
根元素属性:{}
子元素数量:3

3.4 也可以从字符串解析

python 复制代码
import xml.etree.ElementTree as ET

xml_string = """
<student>
    <name>小明</name>
    <age>20</age>
    <grade>大三</grade>
</student>
"""

# fromstring() 直接从字符串解析
root = ET.fromstring(xml_string)

print(root.tag)                    # student
print(root.find("name").text)      # 小明
print(root.find("age").text)       # 20

3.5 遍历所有子元素

python 复制代码
import xml.etree.ElementTree as ET

tree = ET.parse("books.xml")
root = tree.getroot()

# ============ 方法1:直接遍历(只遍历直接子元素) ============
print("=== 方法1:遍历直接子元素 ===")
for book in root:
    print(f"  标签:{book.tag},属性:{book.attrib}")

# 输出:
#   标签:book,属性:{'id': '1', 'category': '编程'}
#   标签:book,属性:{'id': '2', 'category': '文学'}
#   标签:book,属性:{'id': '3', 'category': '编程'}


# ============ 方法2:iter() 遍历所有后代元素(包括子元素的子元素) ============
print("\n=== 方法2:遍历所有后代 ===")
for elem in root.iter():
    print(f"  {elem.tag}: {elem.text.strip() if elem.text and elem.text.strip() else '(无文本)'}")

# 输出(会列出所有层级的元素):
#   bookstore: (无文本)
#   book: (无文本)
#   title: Python编程:从入门到实践
#   author: Eric Matthes
#   ...


# ============ 方法3:iter() 只遍历指定标签 ============
print("\n=== 方法3:只遍历所有 title ===")
for title in root.iter("title"):
    print(f"  📖 {title.text}")

# 输出:
#   📖 Python编程:从入门到实践
#   📖 百年孤独
#   📖 算法导论

3.6 查找元素:find() 和 findall()

这是最常用的操作!

python 复制代码
import xml.etree.ElementTree as ET

tree = ET.parse("books.xml")
root = tree.getroot()

# ============ find():找第一个匹配的元素 ============
# 找第一个 <book>
first_book = root.find("book")
print(f"第一本书的标签:{first_book.tag}")
print(f"第一本书的属性:{first_book.attrib}")

# 找第一个 book 下的 title
title = first_book.find("title")
print(f"书名:{title.text}")

# ============ findall():找所有匹配的元素 ============
# 找所有 <book>
all_books = root.findall("book")
print(f"\n共有 {len(all_books)} 本书")

# 找所有 book 下的 title
all_titles = root.findall("book/title")  # 路径写法!
print("所有书名:")
for t in all_titles:
    print(f"  - {t.text}")

输出

python 复制代码
第一本书的标签:book
第一本书的属性:{'id': '1', 'category': '编程'}
书名:Python编程:从入门到实践

共有 3 本书
所有书名:
  - Python编程:从入门到实践
  - 百年孤独
  - 算法导论

3.7 路径查找语法(重要!)

find()findall() 支持简单的路径表达式:

路径 含义 示例
"book" 直接子元素中找 book root.find("book")
"book/title" book 下的 title root.find("book/title")
".//title" 任意深度的 title root.findall(".//title")
"book[@id='1']" id属性为1的book root.find("book[@id='1']")
"book[title='百年孤独']" title文本为百年孤独的book root.find("book[title='百年孤独']")
"book[1]" 第2个book(索引从0开始) root.find("book[1]")
"book[last()]" 最后一个book root.find("book[last()]")
python 复制代码
import xml.etree.ElementTree as ET

tree = ET.parse("books.xml")
root = tree.getroot()

# 示例1:找任意深度的所有 tag 元素
print("=== 所有 tag ===")
for tag in root.findall(".//tag"):
    print(f"  🏷️  {tag.text}")

# 示例2:找 id="2" 的 book
print("\n=== 找 id=2 的书 ===")
book2 = root.find("book[@id='2']")
print(f"  书名:{book2.find('title').text}")
print(f"  作者:{book2.find('author').text}")

# 示例3:找 category="编程" 的所有书
print("\n=== 所有编程类书籍 ===")
for book in root.findall("book[@category='编程']"):
    print(f"  📗 {book.find('title').text}")

# 示例4:找价格大于某个值的(ElementTree不直接支持数值比较,需要手动过滤)
print("\n=== 价格 > 60 的书 ===")
for book in root.findall("book"):
    price = float(book.find("price").text)
    if price > 60:
        print(f"  💰 {book.find('title').text} - ¥{price}")

输出

python 复制代码
=== 所有 tag ===
  🏷️  Python
  🏷️  入门
  🏷️  小说
  🏷️  魔幻现实主义
  🏷️  算法
  🏷️  计算机

=== 找 id=2 的书 ===
  书名:百年孤独
  作者:加西亚·马尔克斯

=== 所有编程类书籍 ===
  📗 Python编程:从入门到实践
  📗 算法导论

=== 价格 > 60 的书 ===
  💰 Python编程:从入门到实践 - ¥89.0
  💰 算法导论 - ¥128.0

3.8 获取元素的详细信息

python 复制代码
import xml.etree.ElementTree as ET

tree = ET.parse("books.xml")
root = tree.getroot()

book = root.find("book")  # 第一个 book

# ============ 获取标签名 ============
print(f"标签名:{book.tag}")              # book

# ============ 获取属性 ============
print(f"所有属性:{book.attrib}")          # {'id': '1', 'category': '编程'}
print(f"id属性:{book.get('id')}")         # 1
print(f"category属性:{book.get('category')}")  # 编程
print(f"不存在的属性:{book.get('xxx', '默认值')}")  # 默认值

# ============ 获取文本内容 ============
title = book.find("title")
print(f"title的文本:{title.text}")        # Python编程:从入门到实践

# ============ 获取子元素列表 ============
children = list(book)
print(f"book的子元素数量:{len(children)}")  # 5 (title, author, price, year, tags)

for child in children:
    print(f"  子元素:{child.tag} = {child.text}")

# ============ 获取父元素(ElementTree 不直接支持,需要自己建映射) ============
# 方法:遍历建立 parent 映射
parent_map = {child: parent for parent in root.iter() for child in parent}
title_elem = root.find(".//title")
parent = parent_map[title_elem]
print(f"\ntitle 的父元素是:{parent.tag}")  # book

3.9 处理属性(Attribute)

python 复制代码
import xml.etree.ElementTree as ET

tree = ET.parse("books.xml")
root = tree.getroot()

# ============ 读取属性 ============
for book in root.findall("book"):
    book_id = book.get("id")           # 获取单个属性
    category = book.get("category")
    print(f"  ID={book_id}, 类别={category}")

# ============ 读取子元素的属性 ============
for book in root.findall("book"):
    price_elem = book.find("price")
    currency = price_elem.get("currency")  # price 标签上的 currency 属性
    value = price_elem.text
    print(f"  价格:{value} {currency}")

# 输出:
#   价格:89.00 CNY
#   价格:55.00 CNY
#   价格:128.00 USD

3.10 处理命名空间(Namespace)

有些 XML 带有命名空间,解析时会比较麻烦:

python 复制代码
<?xml version="1.0"?>
<root xmlns:ns="http://example.com/schema">
    <ns:item>
        <ns:name>测试</ns:name>
    </ns:item>
</root>
python 复制代码
import xml.etree.ElementTree as ET

xml_string = """<?xml version="1.0"?>
<root xmlns:ns="http://example.com/schema">
    <ns:item>
        <ns:name>测试数据</ns:name>
    </ns:item>
</root>
"""

root = ET.fromstring(xml_string)

# ❌ 这样找不到(因为标签名实际是 "{http://example.com/schema}item")
# item = root.find("ns:item")  # 返回 None

# ✅ 方法1:使用完整命名空间
ns = {"ns": "http://example.com/schema"}
item = root.find("ns:item", ns)
name = item.find("ns:name", ns)
print(f"方法1:{name.text}")  # 测试数据

# ✅ 方法2:用通配符(忽略命名空间)
item = root.find("{*}item")
name = item.find("{*}name")
print(f"方法2:{name.text}")  # 测试数据

# 查看实际的标签名
print(f"实际标签名:{item.tag}")
# 输出:{http://example.com/schema}item

四、创建和修改 XML

4.1 从零创建 XML

python 复制代码
import xml.etree.ElementTree as ET

# ============ 创建根元素 ============
root = ET.Element("students")

# ============ 添加子元素 ============
# 创建第一个学生
student1 = ET.SubElement(root, "student", attrib={"id": "1", "class": "三班"})
name1 = ET.SubElement(student1, "name")
name1.text = "张三"
age1 = ET.SubElement(student1, "age")
age1.text = "20"
score1 = ET.SubElement(student1, "score")
score1.text = "95"

# 创建第二个学生
student2 = ET.SubElement(root, "student", attrib={"id": "2", "class": "一班"})
name2 = ET.SubElement(student2, "name")
name2.text = "李四"
age2 = ET.SubElement(student2, "age")
age2.text = "21"
score2 = ET.SubElement(student2, "score")
score2.text = "88"

# ============ 写入文件 ============
tree = ET.ElementTree(root)

# 方法1:简单写入
tree.write("students.xml", encoding="utf-8", xml_declaration=True)

# 方法2:格式化写入(Python 3.9+)
ET.indent(tree, space="    ")  # 添加缩进
tree.write("students_formatted.xml", encoding="utf-8", xml_declaration=True)

print("✅ XML 文件创建成功!")

生成的 students_formatted.xml

python 复制代码
<?xml version='1.0' encoding='utf-8'?>
<students>
    <student id="1" class="三班">
        <name>张三</name>
        <age>20</age>
        <score>95</score>
    </student>
    <student id="2" class="一班">
        <name>李四</name>
        <age>21</age>
        <score>88</score>
    </student>
</students>

4.2 修改现有 XML

python 复制代码
import xml.etree.ElementTree as ET

tree = ET.parse("books.xml")
root = tree.getroot()

# ============ 修改文本 ============
# 把第一本书的价格改为 99.00
first_book = root.find("book")
price = first_book.find("price")
print(f"修改前价格:{price.text}")   # 89.00
price.text = "99.00"
print(f"修改后价格:{price.text}")   # 99.00

# ============ 修改属性 ============
first_book.set("category", "热门编程")  # 修改已有属性
first_book.set("recommended", "true")   # 添加新属性
print(f"修改后属性:{first_book.attrib}")

# ============ 添加新元素 ============
# 给第一本书添加 <publisher> 元素
publisher = ET.SubElement(first_book, "publisher")
publisher.text = "人民邮电出版社"

# ============ 删除元素 ============
# 删除第一本书的 <year> 元素
year = first_book.find("year")
first_book.remove(year)

# ============ 保存修改 ============
ET.indent(tree, space="    ")
tree.write("books_modified.xml", encoding="utf-8", xml_declaration=True)
print("✅ 修改已保存!")

4.3 在指定位置插入元素

python 复制代码
import xml.etree.ElementTree as ET

root = ET.Element("fruits")

# 添加几个水果
ET.SubElement(root, "fruit").text = "苹果"
ET.SubElement(root, "fruit").text = "香蕉"
ET.SubElement(root, "fruit").text = "橘子"

# 在索引1的位置插入(插在"苹果"和"香蕉"之间)
new_fruit = ET.Element("fruit")
new_fruit.text = "葡萄"
root.insert(1, new_fruit)

# 查看结果
for fruit in root:
    print(fruit.text)

# 输出:
# 苹果
# 葡萄    ← 插入在这里
# 香蕉
# 橘子

五、xml.dom.minidom ------ DOM 方式解析

5.1 什么是 DOM?

DOM (Document Object Model)把整个 XML 加载到内存中,形成一个树形结构,你可以随意遍历、修改任何节点。

和 ElementTree 的区别

  • ElementTree:更 Pythonic,API 更简洁
  • minidom:更接近 W3C 标准,方法名更长,但功能更全

5.2 基本用法

python 复制代码
from xml.dom import minidom

# 解析文件
dom = minidom.parse("books.xml")

# 获取根元素
root = dom.documentElement
print(f"根元素:{root.tagName}")  # bookstore

# 获取所有 book 元素
books = root.getElementsByTagName("book")
print(f"共有 {len(books)} 本书\n")

# 遍历每本书
for book in books:
    # 获取属性
    book_id = book.getAttribute("id")
    category = book.getAttribute("category")
    
    # 获取子元素的文本
    title = book.getElementsByTagName("title")[0].firstChild.data
    author = book.getElementsByTagName("author")[0].firstChild.data
    price = book.getElementsByTagName("price")[0].firstChild.data
    
    print(f"  [{book_id}] 《{title}》 - {author} - ¥{price} ({category})")

输出

python 复制代码
根元素:bookstore
共有 3 本书

  [1] 《Python编程:从入门到实践》 - Eric Matthes - ¥89.00 (编程)
  [2] 《百年孤独》 - 加西亚·马尔克斯 - ¥55.00 (文学)
  [3] 《算法导论》 - Thomas H. Cormen - ¥128.00 (编程)

5.3 minidom 的常用方法对照

操作 ElementTree minidom
解析文件 ET.parse("file.xml") minidom.parse("file.xml")
获取根 tree.getroot() dom.documentElement
标签名 elem.tag node.tagName
文本内容 elem.text node.firstChild.data
获取属性 elem.get("name") node.getAttribute("name")
找子元素 elem.find("title") node.getElementsByTagName("title")
所有子元素 list(elem) node.childNodes

5.4 用 minidom 创建 XML

python 复制代码
from xml.dom import minidom

# 创建文档
doc = minidom.Document()

# 创建根元素
root = doc.createElement("config")
doc.appendChild(root)

# 创建子元素
server = doc.createElement("server")
server.setAttribute("host", "192.168.1.1")
server.setAttribute("port", "8080")
root.appendChild(server)

# 创建带文本的子元素
name = doc.createElement("name")
name_text = doc.createTextNode("我的服务器")
name.appendChild(name_text)
server.appendChild(name)

# 格式化输出
xml_string = doc.toprettyxml(indent="    ", encoding="utf-8")
print(xml_string.decode("utf-8"))

# 保存到文件
with open("config.xml", "w", encoding="utf-8") as f:
    doc.writexml(f, indent="    ", addindent="    ", newl="\n", encoding="utf-8")

输出

python 复制代码
<?xml version="1.0" encoding="utf-8"?>
<config>
    <server host="192.168.1.1" port="8080">
        <name>我的服务器</name>
    </server>
</config>

六、xml.sax ------ 流式解析(处理大文件)

6.1 什么是 SAX?

SAX (Simple API for XML)是事件驱动的解析方式:

  • 不会把整个文件加载到内存
  • 从上往下逐行读取
  • 遇到开始标签、结束标签、文本时触发回调函数

生活比喻

  • DOM/ElementTree = 把整本书搬到桌子上,随便翻
  • SAX = 像听有声书,从头听到尾,听到关键内容就记下来

适合场景:几百MB甚至几GB的XML文件。

6.2 基本用法

python 复制代码
import xml.sax

# ============ 定义事件处理器 ============
class BookHandler(xml.sax.ContentHandler):
    
    def __init__(self):
        super().__init__()
        self.books = []          # 存储所有书
        self.current_book = {}   # 当前正在解析的书
        self.current_tag = ""    # 当前标签名
        self.in_book = False     # 是否在 book 元素内
    
    # 遇到开始标签时触发
    # name: 标签名, attrs: 属性字典
    def startElement(self, name, attrs):
        self.current_tag = name
        
        if name == "book":
            self.in_book = True
            self.current_book = {
                "id": attrs.get("id", ""),
                "category": attrs.get("category", ""),
            }
    
    # 遇到文本内容时触发
    def characters(self, content):
        content = content.strip()
        if not content:
            return
        
        if self.in_book and self.current_tag in ("title", "author", "price", "year"):
            self.current_book[self.current_tag] = content
    
    # 遇到结束标签时触发
    def endElement(self, name):
        if name == "book":
            self.in_book = False
            self.books.append(self.current_book)
            self.current_book = {}
        self.current_tag = ""


# ============ 使用 ============
handler = BookHandler()

# 解析文件
parser = xml.sax.make_parser()
parser.setContentHandler(handler)
parser.parse("books.xml")

# 输出结果
print(f"共解析了 {len(handler.books)} 本书:\n")
for book in handler.books:
    print(f"  [{book['id']}] 《{book.get('title', '?')}》")
    print(f"       作者:{book.get('author', '?')}")
    print(f"       价格:¥{book.get('price', '?')}")
    print(f"       类别:{book.get('category', '?')}")
    print()

输出

python 复制代码
共解析了 3 本书:

  [1] 《Python编程:从入门到实践》
       作者:Eric Matthes
       价格:¥89.00
       类别:编程

  [2] 《百年孤独》
       作者:加西亚·马尔克斯
       价格:¥55.00
       类别:文学

  [3] 《算法导论》
       作者:Thomas H. Cormen
       价格:¥128.00
       类别:编程

6.3 SAX 的回调方法总结

方法 触发时机 参数
startDocument() 文档开始
endDocument() 文档结束
startElement(name, attrs) 遇到 <tag> 标签名、属性
endElement(name) 遇到 </tag> 标签名
characters(content) 遇到文本内容 文本字符串

七、lxml ------ 第三方强大库(可选)

7.1 安装

python 复制代码
pip install lxml

7.2 为什么用 lxml?

  • 支持完整的 XPath(比 ElementTree 的路径语法强大得多)
  • 速度更快(底层用 C 实现)
  • 支持 XSLT 转换
  • API 和 ElementTree 兼容

7.3 基本用法

python 复制代码
from lxml import etree

# 解析文件
tree = etree.parse("books.xml")
root = tree.getroot()

# ============ 强大的 XPath ============

# 找所有价格大于60的书(ElementTree做不到!)
expensive_books = root.xpath("//book[price > 60]/title/text()")
print("价格>60的书:", expensive_books)
# ['Python编程:从入门到实践', '算法导论']

# 找所有编程类书的作者
authors = root.xpath("//book[@category='编程']/author/text()")
print("编程书作者:", authors)
# ['Eric Matthes', 'Thomas H. Cormen']

# 获取所有 tag 的文本
tags = root.xpath("//tag/text()")
print("所有标签:", tags)
# ['Python', '入门', '小说', '魔幻现实主义', '算法', '计算机']

# 获取第一本书的所有属性
attrs = root.xpath("//book[1]/@*")
print("第一本书属性:", attrs)
# ['1', '编程']

# 统计每个类别有几本书
categories = root.xpath("//book/@category")
from collections import Counter
print("类别统计:", dict(Counter(categories)))
# {'编程': 2, '文学': 1}

7.4 XPath 常用语法速查

XPath 表达式 含义
/bookstore/book 根下的直接子 book
//book 任意位置的 book
//book/title 所有 book 下的 title
//book[@id='1'] id=1 的 book
//book[price>60] price 大于 60 的 book
//book[1] 第一个 book
//book[last()] 最后一个 book
//book/@category 所有 book 的 category 属性值
//title/text() 所有 title 的文本内容
//book[contains(title,'Python')] title 包含 Python 的 book
count(//book) book 的数量

八、实战案例

8.1 解析 RSS/Atom 订阅源

python 复制代码
import xml.etree.ElementTree as ET
import urllib.request

def parse_rss(url):
    """解析 RSS 订阅源"""
    # 下载 XML
    response = urllib.request.urlopen(url)
    xml_data = response.read()
    
    # 解析
    root = ET.fromstring(xml_data)
    
    # RSS 结构:rss > channel > item
    channel = root.find("channel")
    title = channel.find("title").text
    print(f"📰 频道:{title}\n")
    
    items = channel.findall("item")
    for i, item in enumerate(items[:5], 1):  # 只显示前5条
        item_title = item.find("title").text
        item_link = item.find("link").text
        pub_date = item.find("pubDate")
        date_str = pub_date.text if pub_date is not None else "未知"
        
        print(f"  {i}. {item_title}")
        print(f"     🔗 {item_link}")
        print(f"     📅 {date_str}")
        print()

# 使用(示例URL,可能失效)
# parse_rss("https://example.com/rss.xml")

8.2 解析 Android 布局文件

python 复制代码
import xml.etree.ElementTree as ET

android_layout = """<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <TextView
        android:id="@+id/title_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World"
        android:textSize="24sp" />
    
    <Button
        android:id="@+id/submit_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="提交" />
    
    <EditText
        android:id="@+id/input_field"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入内容" />
        
</LinearLayout>
"""

# Android XML 有命名空间
ns = {"android": "http://schemas.android.com/apk/res/android"}

root = ET.fromstring(android_layout)

print(f"根布局:{root.tag}")
print(f"方向:{root.get('{http://schemas.android.com/apk/res/android}orientation')}")
print()

# 遍历所有控件
for elem in root:
    tag = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag  # 去掉命名空间前缀
    android_id = elem.get("{http://schemas.android.com/apk/res/android}id", "")
    text = elem.get("{http://schemas.android.com/apk/res/android}text", "")
    
    print(f"  📱 {tag}")
    if android_id:
        print(f"     ID: {android_id}")
    if text:
        print(f"     文本: {text}")

输出

python 复制代码
根布局:{http://schemas.android.com/apk/res/android}LinearLayout
方向:vertical

  📱 TextView
     ID: @+id/title_text
     文本: Hello World
  📱 Button
     ID: @+id/submit_btn
     文本: 提交
  📱 EditText
     ID: @+id/input_field

8.3 XML 转 JSON

python 复制代码
import xml.etree.ElementTree as ET
import json

def xml_to_dict(element):
    """递归将 XML 元素转为字典"""
    result = {}
    
    # 添加属性
    if element.attrib:
        result["@attributes"] = element.attrib
    
    # 处理子元素
    children = list(element)
    if children:
        child_dict = {}
        for child in children:
            child_data = xml_to_dict(child)
            
            # 如果有多个同名子元素,转为列表
            if child.tag in child_dict:
                if not isinstance(child_dict[child.tag], list):
                    child_dict[child.tag] = [child_dict[child.tag]]
                child_dict[child.tag].append(child_data)
            else:
                child_dict[child.tag] = child_data
        
        result.update(child_dict)
    
    # 添加文本
    if element.text and element.text.strip():
        if result:
            result["#text"] = element.text.strip()
        else:
            return element.text.strip()
    
    return result


# 使用
tree = ET.parse("books.xml")
root = tree.getroot()

data = {root.tag: xml_to_dict(root)}
json_string = json.dumps(data, ensure_ascii=False, indent=2)
print(json_string)

# 保存到文件
with open("books.json", "w", encoding="utf-8") as f:
    f.write(json_string)

print("\n✅ 已保存为 books.json")

8.4 批量生成 XML(如:导出报表)

python 复制代码
import xml.etree.ElementTree as ET
from datetime import datetime

def generate_report_xml(students_data, output_file):
    """生成学生成绩报表 XML"""
    
    # 创建根元素
    root = ET.Element("report")
    root.set("generated", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
    root.set("total_students", str(len(students_data)))
    
    # 添加元信息
    meta = ET.SubElement(root, "metadata")
    ET.SubElement(meta, "school").text = "XX大学"
    ET.SubElement(meta, "semester").text = "2025-2026学年第二学期"
    
    # 添加学生数据
    students_elem = ET.SubElement(root, "students")
    
    for student in students_data:
        s = ET.SubElement(students_elem, "student")
        s.set("id", str(student["id"]))
        
        ET.SubElement(s, "name").text = student["name"]
        ET.SubElement(s, "major").text = student["major"]
        
        # 成绩列表
        scores = ET.SubElement(s, "scores")
        for course, score in student["scores"].items():
            score_elem = ET.SubElement(scores, "course")
            score_elem.set("name", course)
            score_elem.text = str(score)
        
        # 计算平均分
        avg = sum(student["scores"].values()) / len(student["scores"])
        ET.SubElement(s, "average").text = f"{avg:.1f}"
    
    # 格式化并保存
    tree = ET.ElementTree(root)
    ET.indent(tree, space="    ")
    tree.write(output_file, encoding="utf-8", xml_declaration=True)
    print(f"✅ 报表已生成:{output_file}")


# 测试数据
students = [
    {
        "id": 1001,
        "name": "张三",
        "major": "计算机科学",
        "scores": {"数学": 92, "英语": 85, "Python": 98}
    },
    {
        "id": 1002,
        "name": "李四",
        "major": "软件工程",
        "scores": {"数学": 78, "英语": 91, "Python": 88}
    },
]

generate_report_xml(students, "report.xml")

生成的 report.xml

python 复制代码
<?xml version='1.0' encoding='utf-8'?>
<report generated="2026-07-29 11:00:00" total_students="2">
    <metadata>
        <school>XX大学</school>
        <semester>2025-2026学年第二学期</semester>
    </metadata>
    <students>
        <student id="1001">
            <name>张三</name>
            <major>计算机科学</major>
            <scores>
                <course name="数学">92</course>
                <course name="英语">85</course>
                <course name="Python">98</course>
            </scores>
            <average>91.7</average>
        </student>
        <student id="1002">
            <name>李四</name>
            <major>软件工程</major>
            <scores>
                <course name="数学">78</course>
                <course name="英语">91</course>
                <course name="Python">88</course>
            </scores>
            <average>85.7</average>
        </student>
    </students>
</report>

九、错误处理

python 复制代码
import xml.etree.ElementTree as ET

# ============ 处理文件不存在 ============
try:
    tree = ET.parse("not_exist.xml")
except FileNotFoundError:
    print("❌ 文件不存在!")

# ============ 处理 XML 格式错误 ============
bad_xml = "<root><unclosed></root>"  # 标签未正确关闭
try:
    root = ET.fromstring(bad_xml)
except ET.ParseError as e:
    print(f"❌ XML 解析错误:{e}")
    # 输出:XML 解析错误:mismatched tag: line 1, column 15

# ============ 处理元素不存在 ============
xml_str = "<root><name>test</name></root>"
root = ET.fromstring(xml_str)

# find() 找不到时返回 None
age = root.find("age")
if age is None:
    print("⚠️ 没有找到 age 元素")
else:
    print(age.text)

# 安全获取文本的辅助函数
def safe_text(element, path, default=""):
    """安全获取元素文本"""
    found = element.find(path)
    if found is not None and found.text:
        return found.text.strip()
    return default

# 使用
name = safe_text(root, "name", "未知")
age = safe_text(root, "age", "0")
print(f"姓名:{name},年龄:{age}")

十、性能对比与选择建议

python 复制代码
import time
import xml.etree.ElementTree as ET
from xml.dom import minidom
from lxml import etree

# 生成一个大 XML 用于测试
def generate_large_xml(filename, count=100000):
    root = ET.Element("data")
    for i in range(count):
        item = ET.SubElement(root, "item", id=str(i))
        ET.SubElement(item, "name").text = f"Item_{i}"
        ET.SubElement(item, "value").text = str(i * 1.5)
    tree = ET.ElementTree(root)
    tree.write(filename)
    print(f"生成了 {count} 条记录的测试文件")

generate_large_xml("large_test.xml", 100000)

# 测试 ElementTree
start = time.time()
tree = ET.parse("large_test.xml")
root = tree.getroot()
count = len(root.findall("item"))
print(f"ElementTree:{time.time()-start:.3f}秒,{count}条")

# 测试 minidom
start = time.time()
dom = minidom.parse("large_test.xml")
items = dom.getElementsByTagName("item")
print(f"minidom:    {time.time()-start:.3f}秒,{len(items)}条")

# 测试 lxml
start = time.time()
tree = etree.parse("large_test.xml")
root = tree.getroot()
count = len(root.findall("item"))
print(f"lxml:       {time.time()-start:.3f}秒,{count}条")

典型结果(仅供参考):

python 复制代码
ElementTree:0.45秒,100000条
minidom:    2.10秒,100000条
lxml:       0.18秒,100000条

选择建议

python 复制代码
┌────────────────────────────────────────────────────────┐
│  文件 < 10MB,日常使用                                  │
│  → xml.etree.ElementTree ✅(够用、简单)               │
├────────────────────────────────────────────────────────┤
│  需要复杂 XPath、XSLT、高性能                            │
│  → lxml ✅                                             │
├────────────────────────────────────────────────────────┤
│  文件 > 100MB,内存有限                                 │
│  → xml.sax(流式解析)✅                                │
├────────────────────────────────────────────────────────┤
│  需要 W3C 标准 DOM 操作                                 │
│  → xml.dom.minidom                                     │
└────────────────────────────────────────────────────────┘

十一、常见问题 FAQ

Q1:解析时中文乱码?

python 复制代码
# 确保文件编码和声明一致
# XML 声明写 encoding="UTF-8",文件也要保存为 UTF-8

# 读取时指定编码
with open("file.xml", "r", encoding="utf-8") as f:
    content = f.read()
root = ET.fromstring(content)

Q2:text 返回 None?

python 复制代码
# 如果元素没有文本内容(只有子元素),text 是 None
# <book><title>xxx</title></book>
# book.text → None(因为 book 和 title 之间只有空白)
# book.find("title").text → "xxx"

# 安全写法:
text = elem.text or ""  # 如果是 None 就用空字符串
text = (elem.text or "").strip()  # 还要去掉首尾空白

Q3:如何忽略命名空间?

python 复制代码
# 方法:解析前去掉命名空间
import re

def remove_namespace(xml_string):
    """去掉 XML 中的所有命名空间"""
    return re.sub(r'xmlns[^=]*="[^"]*"', '', xml_string)

clean_xml = remove_namespace(raw_xml)
root = ET.fromstring(clean_xml)

Q4:ElementTree 和 lxml 的 API 兼容吗?

python 复制代码
# 大部分兼容!切换只需改导入:

# ElementTree
import xml.etree.ElementTree as ET

# lxml(API 几乎一样)
from lxml import etree as ET

# 后面的代码基本不用改
tree = ET.parse("file.xml")
root = tree.getroot()

十二、速查表

python 复制代码
解析文件:      tree = ET.parse("file.xml")
解析字符串:    root = ET.fromstring(xml_string)
获取根元素:    root = tree.getroot()
标签名:        elem.tag
文本内容:      elem.text
属性:          elem.get("name") / elem.attrib
找第一个:      elem.find("path")
找所有:        elem.findall("path")
任意深度找:    elem.findall(".//tag")
按属性找:      elem.find("tag[@attr='value']")
遍历所有后代:  for e in root.iter():
遍历指定标签:  for e in root.iter("title"):
创建元素:      ET.Element("tag")
创建子元素:    ET.SubElement(parent, "tag")
设置文本:      elem.text = "内容"
设置属性:      elem.set("key", "value")
添加子元素:    parent.append(child)
删除子元素:    parent.remove(child)
插入元素:      parent.insert(index, child)
保存到文件:    tree.write("out.xml", encoding="utf-8", xml_declaration=True)
格式化缩进:    ET.indent(tree, space="    ")  # Python 3.9+

十三、学习路径建议

python 复制代码
第1步:理解 XML 的结构(标签、属性、文本、嵌套)
第2步:用 ET.parse() 解析文件,用 find/findall 查找元素
第3步:用 iter() 遍历,用 get() 获取属性
第4步:学习路径语法(.//tag、[@attr='val'])
第5步:练习创建和修改 XML
第6步:了解 minidom 和 SAX 的区别
第7步:(进阶)学习 lxml 和 XPath
第8步:实战(解析配置文件、RSS、API返回的XML等)
相关推荐
万亿少女的梦1681 小时前
基于Spring Boot、Vue与MySQL的私人健身教练预约管理系统设计
java·spring boot·mysql·vue·预约管理
绝世唐门三哥1 小时前
CSS 虚线下划线用法指南:text-decoration 完整解析
前端·javascript·css
程序员爱钓鱼1 小时前
Go if 判断详解
前端·后端·go
程序员黑豆1 小时前
Java字符串拼接全解析:6种方式性能对比与实战指南
java·前端·ai编程
万少6 小时前
DeepSeek 昨晚刚开源了 Harness:附万少的2 万字保姆级教程
前端·后端·架构
程序员黑豆8 小时前
Java字符串详解
java·前端·ai编程
无我Code9 小时前
开发者-2026年中总结
前端·面试·程序员
EntyIU9 小时前
NFS离线部署
java
浮生望10 小时前
React 受控与非受控组件:从表单状态管理到实时校验的完整实践
前端