pandas iterparse 读取重名 XML 节点时丢值、错列
read_xml 的 iterparse 同时读取两个同名子节点时,大文件中可能出现缺值或错列。按 row 结束事件读取可避开提前占列;最终 DataFrame 仍需容纳整表。
使用 pd.read_xml() 的 iterparse 读取几 MB 的 XML,绝大多数行正常,少数行却出现第一列为空、第一项跑到第二列,或第二项直接缺失。如果一行有两个同名子节点,并用 names 分配为不同列,应先检查重复节点的解析路径。小文件正常,不能排除这个问题。
已出现该现象的环境是 pandas 3.0.6,lxml 与 etree 都可能受到影响。pandas 3.1.0 的重复节点分支仍同时处理 start 和 end 事件,因此不能把升级到 3.1.0 当作这条路径已经修好的保证。错误行数与行宽、解析缓冲边界有关,不是固定比例。
两个同名节点为何会占错列
下面是输入结构与对应调用,rows.xml 是应用已有的本地、未压缩 XML 文件:
<data>
<row><a>first</a><a>second</a></row>
</data>
import pandas as pd
frame = pd.read_xml(
"rows.xml",
iterparse={"row": ["a", "a"]},
names=["a1", "a2"],
)
预期每行的两个 a 分别进入 a1、a2。但解析器发出元素开始事件时,文本未必已经读取完整。重复名称分支会把此时的 None 写入尚未使用的列名;等到结束事件得到真正文本,前一个列名已经被占用,数据便可能进入下一列。这个分支还有按值排除重复的判断,也不适合把相同文本的两个位置视为两个独立字段。
因此,给结果 fillna() 或向前填充会掩盖数据丢失;把 lxml 换成 etree 也不能从机制上解决。先保留原文件与有问题的行,避免错误结果覆盖唯一的数据副本。
固定结构可以在 row 结束时按位置取值
如果 XML 来自可信输入,且协议明确每个 row 恰有两个直接子节点 a,可以单独按行结束事件读取。下面的函数针对无命名空间的这个结构;发现节点数量不符就停止,不猜测字段如何补齐:
import xml.etree.ElementTree as ET
import pandas as pd
def records(path):
with open(path, "rb") as source:
context = ET.iterparse(source, events=("start", "end"))
_, root = next(context)
for event, elem in context:
if event == "end" and elem.tag == "row":
values = [child.text for child in elem if child.tag == "a"]
if len(values) != 2:
raise ValueError("each row must contain exactly two a elements")
yield {"a1": values[0], "a2": values[1]}
elem.clear()
root.clear()
frame = pd.DataFrame.from_records(records("rows.xml"))
这里在整行结束后取两个位置,不按文本是否相同去重。root.clear() 以 row 是根的直接子节点、根上没有待保留业务信息为前提;嵌套行、命名空间、混合文本或需要保留根属性的格式,应按自己的 XML 协议调整。不要把这段读取器用于未经评估的不可信 XML。
这种写法减少了已处理 XML 节点的保留,但最终 DataFrame 仍装下全部行。数据本身放不进内存时,应把记录按批交给下游,而不是认为用了生成器就没有整表内存开销。
如果文件连同解析树能安全放进内存,也可改用 read_xml 的 xpath 路径读取固定子节点,并单独核对同名元素的顺序。那会改变内存占用,不能直接拿来替换大文件的流式任务。
用跨缓冲边界的数据验收
验收要包含足够多、宽度不同的行,并让两列携带可区分的值;同时加入两项文本恰好相同的行。逐行比较两个字段、行数与缺失位置,比只统计空值更有效。合法空节点也需要单独约定,不能把所有空值都判为解析故障。
已经生成的错列表格应从 XML 原件重读。修正读取代码不会恢复先前丢掉的第二个值。


