一、什么是 XML?
XML(eXtensible Markup Language,可扩展标记语言)是一种用来存储和传输数据的文本格式。
1.1 生活比喻
把 XML 想象成一个带标签的收纳盒:
1.2 XML 长什么样?
<?xml version="1.0" encoding="UTF-8"?><bookstore> <bookcategory="编程"> <title>Python入门</title> <author>张三</author> <price>59.9</price> <year>2025</year> </book> <bookcategory="文学"> <title>红楼梦</title> <author>曹雪芹</author> <price>39.0</price> <year>1791</year> </book></bookstore>
1.3 XML 的基本结构
<?xml version="1.0" encoding="UTF-8"?> ← XML 声明(可选)<bookstore> ← 根元素(必须有且只有一个) <bookcategory="编程"> ← 子元素,category 是"属性" <title>Python入门</title> ← 子元素,"Python入门"是"文本内容" <author>张三</author> <price>59.9</price> </book></bookstore>
1.4 XML 的术语对照表
<person name="张三" age="25"> ← "person" 是元素(标签) <city>北京</city> ← "name"、"age" 是属性</person> ← "北京" 是文本内容
| | | |
|---|
| | <book>...</book> | |
| | book | |
| | category="编程" | |
| | Python入门 | |
| | <bookstore> | |
| | <book> | |
| | <bookstore> | |
| | | |
1.5 XML 的规则
✅ 必须有且只有一个根元素✅ 标签必须成对关闭:<title>...</title>✅ 标签必须正确嵌套:<a><b></b></a> ✅ <a><b></a></b> ❌✅ 属性值必须加引号:name="张三" ✅ name=张三 ❌✅ 标签名区分大小写:<Title> 和 <title> 是不同的
二、Python 解析 XML 的方式概览
Python 提供了多种解析 XML 的方式:
| | | |
|---|
| ElementTree | xml.etree.ElementTree | | |
| minidom | xml.dom.minidom | | |
| SAX | xml.sax | | |
| lxml | lxml | | |
本教程重点讲 ElementTree(最常用),其他方式也会介绍。
三、ElementTree —— 最推荐的方式
3.1 导入模块
import xml.etree.ElementTree as ET
约定俗成:用 ET 作为别名。
3.2 准备一个 XML 文件
创建文件 books.xml:
<?xml version="1.0" encoding="UTF-8"?><bookstore> <bookid="1"category="编程"> <title>Python编程:从入门到实践</title> <author>Eric Matthes</author> <pricecurrency="CNY">89.00</price> <year>2020</year> <tags> <tag>Python</tag> <tag>入门</tag> </tags> </book> <bookid="2"category="文学"> <title>百年孤独</title> <author>加西亚·马尔克斯</author> <pricecurrency="CNY">55.00</price> <year>1967</year> <tags> <tag>小说</tag> <tag>魔幻现实主义</tag> </tags> </book> <bookid="3"category="编程"> <title>算法导论</title> <author>Thomas H. Cormen</author> <pricecurrency="USD">128.00</price> <year>2009</year> <tags> <tag>算法</tag> <tag>计算机</tag> </tags> </book></bookstore>
3.3 解析 XML 文件(从文件读取)
import xml.etree.ElementTree as ET# ============ 第一步:解析文件,得到"树"对象 ============tree = ET.parse("books.xml")# ============ 第二步:获取根元素 ============root = tree.getroot()# 查看根元素的信息print(f"根元素标签:{root.tag}") # bookstoreprint(f"根元素属性:{root.attrib}") # {}(根元素没有属性)print(f"子元素数量:{len(root)}") # 3(有3个book)
输出:
根元素标签:bookstore根元素属性:{}子元素数量:3
3.4 也可以从字符串解析
import xml.etree.ElementTree as ETxml_string = """<student> <name>小明</name> <age>20</age> <grade>大三</grade></student>"""# fromstring() 直接从字符串解析root = ET.fromstring(xml_string)print(root.tag) # studentprint(root.find("name").text) # 小明print(root.find("age").text) # 20
3.5 遍历所有子元素
import xml.etree.ElementTree as ETtree = 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()
这是最常用的操作!
import xml.etree.ElementTree as ETtree = 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 下的 titletitle = first_book.find("title")print(f"书名:{title.text}")# ============ findall():找所有匹配的元素 ============# 找所有 <book>all_books = root.findall("book")print(f"\n共有 {len(all_books)} 本书")# 找所有 book 下的 titleall_titles = root.findall("book/title") # 路径写法!print("所有书名:")for t in all_titles: print(f" - {t.text}")
输出:
第一本书的标签:book第一本书的属性:{'id': '1', 'category': '编程'}书名:Python编程:从入门到实践共有 3 本书所有书名: - Python编程:从入门到实践 - 百年孤独 - 算法导论
3.7 路径查找语法(重要!)
find() 和 findall() 支持简单的路径表达式:
| | |
|---|
"book" | | root.find("book") |
"book/title" | | root.find("book/title") |
".//title" | | root.findall(".//title") |
"book[@id='1']" | | root.find("book[@id='1']") |
"book[title='百年孤独']" | | root.find("book[title='百年孤独']") |
"book[1]" | | root.find("book[1]") |
"book[last()]" | | root.find("book[last()]") |
import xml.etree.ElementTree as ETtree = ET.parse("books.xml")root = tree.getroot()# 示例1:找任意深度的所有 tag 元素print("=== 所有 tag ===")for tag in root.findall(".//tag"): print(f" 🏷️ {tag.text}")# 示例2:找 id="2" 的 bookprint("\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}")
输出:
=== 所有 tag === 🏷️ Python 🏷️ 入门 🏷️ 小说 🏷️ 魔幻现实主义 🏷️ 算法 🏷️ 计算机=== 找 id=2 的书 === 书名:百年孤独 作者:加西亚·马尔克斯=== 所有编程类书籍 === 📗 Python编程:从入门到实践 📗 算法导论=== 价格 > 60 的书 === 💰 Python编程:从入门到实践 - ¥89.0 💰 算法导论 - ¥128.0
3.8 获取元素的详细信息
import xml.etree.ElementTree as ETtree = 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')}") # 1print(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)
import xml.etree.ElementTree as ETtree = 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 带有命名空间,解析时会比较麻烦:
<?xml version="1.0"?><rootxmlns:ns="http://example.com/schema"> <ns:item> <ns:name>测试</ns:name> </ns:item></root>
import xml.etree.ElementTree as ETxml_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
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:
<?xml version='1.0' encoding='utf-8'?><students> <studentid="1"class="三班"> <name>张三</name> <age>20</age> <score>95</score> </student> <studentid="2"class="一班"> <name>李四</name> <age>21</age> <score>88</score> </student></students>
4.2 修改现有 XML
import xml.etree.ElementTree as ETtree = ET.parse("books.xml")root = tree.getroot()# ============ 修改文本 ============# 把第一本书的价格改为 99.00first_book = root.find("book")price = first_book.find("price")print(f"修改前价格:{price.text}") # 89.00price.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 在指定位置插入元素
import xml.etree.ElementTree as ETroot = 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 基本用法
from xml.dom import minidom# 解析文件dom = minidom.parse("books.xml")# 获取根元素root = dom.documentElementprint(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})")
输出:
根元素:bookstore共有 3 本书 [1] 《Python编程:从入门到实践》 - Eric Matthes - ¥89.00 (编程) [2] 《百年孤独》 - 加西亚·马尔克斯 - ¥55.00 (文学) [3] 《算法导论》 - Thomas H. Cormen - ¥128.00 (编程)
5.3 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
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")
输出:
<?xml version="1.0" encoding="utf-8"?><config> <serverhost="192.168.1.1"port="8080"> <name>我的服务器</name> </server></config>
六、xml.sax —— 流式解析(处理大文件)
6.1 什么是 SAX?
SAX(Simple API for XML)是事件驱动的解析方式:
生活比喻:
适合场景:几百MB甚至几GB的XML文件。
6.2 基本用法
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()
输出:
共解析了 3 本书: [1] 《Python编程:从入门到实践》 作者:Eric Matthes 价格:¥89.00 类别:编程 [2] 《百年孤独》 作者:加西亚·马尔克斯 价格:¥55.00 类别:文学 [3] 《算法导论》 作者:Thomas H. Cormen 价格:¥128.00 类别:编程
6.3 SAX 的回调方法总结
| | |
|---|
startDocument() | | |
endDocument() | | |
startElement(name, attrs) | | |
endElement(name) | | |
characters(content) | | |
七、lxml —— 第三方强大库(可选)
7.1 安装
7.2 为什么用 lxml?
- 支持完整的 XPath(比 ElementTree 的路径语法强大得多)
7.3 基本用法
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 Counterprint("类别统计:", dict(Counter(categories)))# {'编程': 2, '文学': 1}
7.4 XPath 常用语法速查
| |
|---|
/bookstore/book | |
//book | |
//book/title | |
//book[@id='1'] | |
//book[price>60] | |
//book[1] | |
//book[last()] | |
//book/@category | |
//title/text() | |
//book[contains(title,'Python')] | |
count(//book) | |
八、实战案例
8.1 解析 RSS/Atom 订阅源
import xml.etree.ElementTree as ETimport urllib.requestdef 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 布局文件
import xml.etree.ElementTree as ETandroid_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}")
输出:
根布局:{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
import xml.etree.ElementTree as ETimport jsondef 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(如:导出报表)
import xml.etree.ElementTree as ETfrom datetime import datetimedef 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:
<?xml version='1.0' encoding='utf-8'?><reportgenerated="2026-07-29 11:00:00"total_students="2"> <metadata> <school>XX大学</school> <semester>2025-2026学年第二学期</semester> </metadata> <students> <studentid="1001"> <name>张三</name> <major>计算机科学</major> <scores> <coursename="数学">92</course> <coursename="英语">85</course> <coursename="Python">98</course> </scores> <average>91.7</average> </student> <studentid="1002"> <name>李四</name> <major>软件工程</major> <scores> <coursename="数学">78</course> <coursename="英语">91</course> <coursename="Python">88</course> </scores> <average>85.7</average> </student> </students></report>
九、错误处理
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() 找不到时返回 Noneage = 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}")
十、性能对比与选择建议
import timeimport xml.etree.ElementTree as ETfrom xml.dom import minidomfrom 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)# 测试 ElementTreestart = time.time()tree = ET.parse("large_test.xml")root = tree.getroot()count = len(root.findall("item"))print(f"ElementTree:{time.time()-start:.3f}秒,{count}条")# 测试 minidomstart = time.time()dom = minidom.parse("large_test.xml")items = dom.getElementsByTagName("item")print(f"minidom: {time.time()-start:.3f}秒,{len(items)}条")# 测试 lxmlstart = time.time()tree = etree.parse("large_test.xml")root = tree.getroot()count = len(root.findall("item"))print(f"lxml: {time.time()-start:.3f}秒,{count}条")
典型结果(仅供参考):
ElementTree:0.45秒,100000条minidom: 2.10秒,100000条lxml: 0.18秒,100000条
选择建议
┌────────────────────────────────────────────────────────┐│ 文件 < 10MB,日常使用 ││ → xml.etree.ElementTree ✅(够用、简单) │├────────────────────────────────────────────────────────┤│ 需要复杂 XPath、XSLT、高性能 ││ → lxml ✅ │├────────────────────────────────────────────────────────┤│ 文件 > 100MB,内存有限 ││ → xml.sax(流式解析)✅ │├────────────────────────────────────────────────────────┤│ 需要 W3C 标准 DOM 操作 ││ → xml.dom.minidom │└────────────────────────────────────────────────────────┘
十一、常见问题 FAQ
Q1:解析时中文乱码?
# 确保文件编码和声明一致# XML 声明写 encoding="UTF-8",文件也要保存为 UTF-8# 读取时指定编码withopen("file.xml", "r", encoding="utf-8") as f: content = f.read()root = ET.fromstring(content)
Q2:text 返回 None?
# 如果元素没有文本内容(只有子元素),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:如何忽略命名空间?
# 方法:解析前去掉命名空间import redef 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 兼容吗?
# 大部分兼容!切换只需改导入:# ElementTreeimport xml.etree.ElementTree as ET# lxml(API 几乎一样)from lxml import etree as ET# 后面的代码基本不用改tree = ET.parse("file.xml")root = tree.getroot()
十二、速查表
解析文件: 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+
十三、学习路径建议
第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等)