pandas.read_xml()仅支持极简xml结构,遇嵌套、命名空间等必失败;应改用xml.etree.elementtree或lxml手动解析后构建dataframe。

直接用 pandas.read_xml() 会失败,除非 XML 结构极简单
新版 Pandas(1.3+)确实加了 read_xml(),但它对 XML 的容忍度很低:只支持扁平、无嵌套、无命名空间、无重复同名子节点的结构。一旦遇到 <person><name>Alice</name><address><city>Beijing</city></address></person> 这类嵌套,它要么报错 ValueError: Unstacked DataFrame is not supported,要么把整个 <address></address> 当成字符串塞进一列,丢失内部结构。
真正可靠的做法是绕过 read_xml(),用标准库 xml.etree.ElementTree 或第三方 lxml 手动解析,再按需构造字典列表,最后喂给 pd.DataFrame()。
用 xml.etree.ElementTree 提取嵌套字段并展平
这是最轻量、无需额外安装的方案。关键在递归遍历或用 .findall() 定位重复节点,再用 .text 或 .get() 取值。注意空值、类型转换和重复标签处理。
- 用
root.findall('record')获取所有顶层记录节点(别用findall('.//record'),性能差且易误匹配) - 对每个
record,用elem.find('name')取子元素,.text获取文本,.get('id')取属性 - 嵌套字段如
<address><city>Shanghai</city></address>,需两级调用:record.find('address').find('city').text if record.find('address') is not None else None - 避免直接用
record.findtext('name')—— 它返回None时无法区分“没找到”和“值为空字符串”
import xml.etree.ElementTree as ET
import pandas as pd
xml_str = """<data>
<record id="1">
<name>Alice</name>
<age>30</age>
<address><city>Beijing</city><zip>100000</zip></address>
</record>
<record id="2">
<name>Bob</name>
<age>25</age>
<address><city>Shanghai</city><zip>200000</zip></address>
</record>
</data>"""
root = ET.fromstring(xml_str)
records = []
for record in root.findall('record'):
city_elem = record.find('address').find('city') if record.find('address') is not None else None
zip_elem = record.find('address').find('zip') if record.find('address') is not None else None
records.append({
'id': record.get('id'),
'name': record.find('name').text if record.find('name') is not None else None,
'age': int(record.find('age').text) if record.find('age') is not None else None,
'city': city_elem.text if city_elem is not None else None,
'zip': zip_elem.text if zip_elem is not None else None
})
df = pd.DataFrame(records)
用 lxml 和 XPath 处理带命名空间或复杂路径的 XML
当 XML 含命名空间(如 xmlns="http://example.com/ns")或需要模糊匹配(如 //item[@type='user']),xml.etree.ElementTree 的 XPath 支持太弱,必须换 lxml。它支持完整 XPath 1.0,且性能更好。
立即学习“Python免费学习笔记(深入)”;
- 注册命名空间:用
namespaces={'ns': 'http://example.com/ns'},然后在 XPath 中写ns:record - 用
tree.xpath('//record')替代findall(),支持更灵活的条件筛选 -
tree.xpath('string(./name)')可安全取文本,自动处理缺失节点(返回空字符串) - 注意
lxml默认不校验 XML,若数据不可信,加parser = etree.XMLParser(recover=True)
from lxml import etree
import pandas as pd
xml_str = '''<?xml version="1.0"?>
<root xmlns="http://example.com/ns">
<record><name>Charlie</name></record>
</root>'''
parser = etree.XMLParser()
tree = etree.fromstring(xml_str, parser)
namespaces = {'ns': 'http://example.com/ns'}
records = []
for record in tree.xpath('//ns:record', namespaces=namespaces):
records.append({
'name': record.xpath('string(ns:name)', namespaces=namespaces).strip() or None
})
df = pd.DataFrame(records)
性能与内存:大文件别一次性加载到内存
XML 文件超过 10MB 时,ET.fromstring() 或 etree.parse() 会吃光内存。此时必须用流式解析(iterparse() 或 lxml.etree.iterparse()),边读边建行,及时清理已处理节点。
-
iterparse()返回 (event, elem) 元组,监听'start'或'end'事件 - 遇到
'end'且elem.tag == 'record'时,提取数据并调用elem.clear()释放内存 - 不要在循环里反复调用
root.findall()—— 它每次都会重新遍历整棵树 - 如果只是提取固定字段,用 SAX 解析器(
xml.sax)更省内存,但编码复杂度上升
嵌套层级深、含命名空间、字段类型不一致、文件体积大——这些不是边缘情况,而是真实 XML 数据的常态。手动解析看似多写几行,实则省去调试 read_xml() 报错的时间,也避开了隐式类型转换带来的陷阱。










