python解析XML格式数据
参考: https://docs.python.org/zh-tw/3.11/library/xml.etree.elementtree.html
背景
ElementTree XML API主要是xml.etree.ElementTree模块,该模块实现了一个简单高效的API,用于解析和创建XML数据。
本博客内容,是使用xml.etree.ElementTree(以下简称ET)的简短教程,目标是演示模块的一些构建块和基本概念,再加上我自己的一个实战数据库清洗实例。
另外,如果xml入门了,其实学html也就简单了。
XML树和元素
XML是一种继承性的分层数据格式,最自然的表示方法是使用树。
为此,ET有两个类:
- ElementTree将整个XML文档表示为1个树
- Element表示该树中的单个节点
与整个文档的交互(读写文件)通常是在ElementTree级别完成的,与单个XML元素及其子元素的交互是在Element级别完成的。
解析XML
我将使用我本人的一个示例数据来进行演示,
此处使用vscode拓展来展示xml格式数据的文本内容:

内容展示如下:

然后这个xml格式的chart参考官网:https://www.ideal-db.org/chart.html
可以看到这个数据格式比较复杂:

主要格式如下

我们可以通过从文件中读取来导入此数据:
import xml.etree.ElementTree as ET
tree = ET.parse("该xml文件路径")
root = tree.getroot()

除了从文件中读取xml,我们还可以直接从字符串中解析,
也就是对1个xml内容的文本文档进行解析,
官方教程示例:https://docs.python.org/zh-tw/3.11/library/xml.etree.elementtree.html

效果如下:

总而言之,两种解析方法:
| 方法 | 输入 | 返回 |
|---|---|---|
ET.parse(file) |
XML 文件 | ElementTree对象 |
ET.fromstring(xml_str) |
XML 字符串 | 根节点 Element |

所以:
- parse要多一步:.getroot()
- fromstring直接返回root
fromstring()将XML 从字符串直接解析为Element,该元素是已解析树的根元素。 其他解析函数可能会创建一个ElementTree。 确切信息请查阅文档。
然后作为Element,root具有标签和属性字典:
root.tag, root.attrib, len(root)
因为我这爬的是一个数据库

还有可以迭代的子节点,然后子节点同样有标签和属性字典:
for child in root:
print(child.tag, child.attrib)

比如说这一段,我想打印每个entry的标签+属性+文本内容,
for child in root:
# 打印标签和属性
print(f"标签: {child.tag}, 属性: {child.attrib}")
# 打印这个节点里的文本内容
if child.text is not None:
print(f"内容: {child.text.strip()}")
print("-" * 40)
比如说,我们能够看到这个数据库的版本是去年2月更新的,如果对照官方数据库更新时间戳发现比较旧,我们就可以换新的数据。

然后子级是可以嵌套的,我们可以通过索引访问特定的子级节点:
比如说root[0]就是version节点


注意:并非XML 输入的所有元素都将作为解析树的元素结束。 目前,此模块跳过输入中的任何XML 注释、处理指令和文档类型声明。 然而,使用这个模块的API 而不是从XML 文本解析构建的树可以包含注释和处理指令,生成XML 输出时同样包含这些注释和处理指令。 可以通过将自定义TreeBuilder实例传递给XMLParser构造器来访问文档类型声明。
用于非阻塞解析的拉取API
简单来讲 Python xml.etree.ElementTree 里的「非阻塞、增量式 XML 解析」,重点是 XMLPullParser。
1,普通解析的问题
- 普通 parse / fromstring 都是一次性读完整个文档才返回结果。
- 大文件、网络流会阻塞、占内存。
2,推送式 API(XMLParser)的问题
- XMLParser 是推送式:解析器主动把数据推给你的回调函数。
- 不够灵活,不好控制流程。
3,XMLPullParser 是什么?
拉取式、非阻塞、增量解析 API
- 我用
feed()一段一段喂 XML 数据(比如从网络分片接收)。 - 我用
read_events()主动去拉解析结果(start/end 事件 + Element 对象)。 - 不用等全部读完,边收边解析,不阻塞。
特点:
- 适合:网络套接字、流式读取、不能阻塞的场景。
- 能拿到完整的 Element 对象,不是裸字符串。
举个例子:
parser = XMLPullParser(['start', 'end'])
parser.feed('<mytag>sometext') # 喂第一段
→ 读到 start 事件
parser.feed(' more text</mytag>') # 喂第二段
→ 读到 end 事件,拿到完整元素
4,和 iterparse 的区别
- XMLPullParser:完全非阻塞,适合网络流。
- iterparse:会阻塞读取,但也能增量解析、省内存,适合超大本地 XML 文件。
5,flush() 作用
flush()强制立即解析已喂的数据,降低延迟。- 要注意安全说明。
总而言之,XMLPullParser就是一个:我们可以一段段喂数据、一段段取解析结果,不阻塞、还能拿到完整Element的流式XML解析器。
查找感兴趣的元素
Element有一些很有效的方法,可以帮助递归遍历其下的所有子树(包括子级,子级的子级,等等),比如说Element.iter():
我们的xml结构大致如下
<?xml version="1.0" encoding="UTF-8"?>
<IDEAL>
<version>20250201</version>
<IDEAL_entry>
<date>
<pdb_dataset>20180803</pdb_dataset>
<uniprot_dataset>20180425</uniprot_dataset>
<ideal_dataset>20250201</ideal_dataset>
</date>
<idp_id>IID00001</idp_id>
<General>
<name>Nuclear receptor subfamily 5 group A member 2</name>
<synonym>Alpha-1-fetoprotein transcription factor</synonym>
<synonym>B1-binding factor</synonym>
<synonym>CYP7A promoter-binding factor</synonym>
<synonym>Hepatocytic transcription factor</synonym>
<synonym>Liver receptor homolog 1</synonym>
<source_organism>Homo sapiens</source_organism>
<sequence_length>541</sequence_length>
<uniprot>Q00482</uniprot>
<uniprot>B4E2P3</uniprot>
<uniprot>O95642</uniprot>
<uniprot>Q147U3</uniprot>
<function>Nuclear receptor that acts as a key metabolic sensor by regulating the expression of genes involved in bile acid synthesis, cholesterol homeostasis</function>
<subcellular_location>
<location_name>Nucleus.</location_name>
<location_ac>SL-0191</location_ac>
<location_llps>unkown</location_llps>
</subcellular_location>
<motif>
<motif_name>FTZ-F1 box</motif_name>
<motif_region>
<motif_region_start>155</motif_region_start>
<motif_region_end>184</motif_region_end>
</motif_region>
</motif>
<gene_symbol>NR5A2</gene_symbol>
<entry_name>Nuclear receptor subfamily 5 gro</entry_name>
<sequence>MSSNSDTGDLQESLK HGLTPI GAGLPDRH GSPIPAR GRLVMLPKVETEALGARSHGEQGGQMPENMQVSQFKMVNYSYDEDLEELCPVCGDKVSGYHYGLLTCESCKGFFKRTVQNNKRYTCIENQNCQIDKTQKRKRCPCYRFQKC</sequence>
</General>
<Region>
<region_id>1</region_id>
<chain_id>1yokaA</chain_id>
<region_start>300</region_start>
<region_end>334</region_end>
<order_disorder>order</order_disorder>
<condition_id>1</condition_id>
</Region>
</IDEAL_entry>
</IDEAL>

按照上图所示,比如说我们可以只遍历某一类标签,比如说所有<idp_id>,
for idp_id in root.iter("idp_id"):
print(idp_id.text, "\n")

Element.findall()仅查找当前元素的直接子元素中带有指定标签的元素。Element.find()找带有特定标签的第一个子级,然后可以用Element.TEXT访问元素的文本内容,Element.get访问元素的属性。
比如说我想访问每一个IDEAL entry下面general条目其中motif项的motif_region信息,
我就可以
for motif_region in root.findall("IDEAL_entry/General/motif/motif_region"):
start = motif_region.find("motif_region_start").text
end = motif_region.find("motif_region_end").text
print(f"{start}-{end}\n")

通过使用XPath,可以更精确地指定要查找的元素。
然后对于我的这个例子,我知道root节点也就是IDEAL节点下有很多直接子节点,
其中比较重要的好似IDEAL_entry,也就相当于是每一个具体的我们需要知道的条目数据。
然后我们一开始是不知道有哪些子节点的,一般如果数据不是很大的话,可以尝试遍历所有的<IDEAL_entry>并打印其所有子节点内容,
比如说我们这里就可以直接打印前5个entry,看看具体每一个entry里面都有哪些内容、树状结构是怎么组织的。
# 遍历所有 IDEAL_entry
for entry in root.findall('IDEAL_entry')[:5]: # 只看前5个条目
print(f"=== IDEAL_entry 开始 ===")
# 递归遍历 entry 下的所有子节点(包括子子节点)
elem_count = 0
for elem in entry.iter('*'):
if elem.text and elem.text.strip(): # 只打印有文本内容的节点
print(f"{elem.tag}: {elem.text.strip()}")
elem_count += 1
print(f"=== IDEAL_entry 结束 (共 {elem_count} 个元素) ===\n")
print(f"=== IDEAL_entry 结束 ===\n")

XML解析的核心工具函数、属性
前面提到的几个核心函数,Element.iter/findall/find/get,以及几个核心属性,element.tag/attrib/text等,是解析XML的核心工具,此处还是展开来比对讲讲。
| 工具类型 | 名称 | 核心作用 | 关键特点 |
|---|---|---|---|
| 遍历函数 | Element.iter(tag) |
递归遍历当前元素下所有层级的指定标签元素(子级、孙级、曾孙级…) | 1. 递归(穿透所有层级);2. 可指定标签,不指定则遍历所有元素;3. 返回迭代器 |
| 查找函数 | Element.findall() |
仅查找当前元素的直接子元素中符合指定标签 / XPpath 的元素 | 1. 非递归(只查一级);2. 返回列表;3. 支持 XPath 精准定位 |
| 查找函数 | Element.find() |
仅查找当前元素的直接子元素中符合指定标签 / XPpath 的第一个元素 | 1. 非递归;2. 返回单个元素(无则返回 None);3. 支持 XPath |
| 属性函数 | Element.get(key) |
获取当前元素的指定属性值(XML 标签上的键值对,如 <a id="123">中的 id) |
1. 只查当前元素的属性;2. 无该属性则返回 None |
| 核心属性 | Element.tag |
获取当前元素的标签名(如 <IDEAL_entry>的 tag 是 “IDEAL_entry”) |
只读属性,返回字符串 |
| 核心属性 | Element.attrib |
获取当前元素的所有属性,返回字典(如 <entry id="001">→ {"id":"001"}) |
只读属性,返回字典,可通过键取值(如 elem.attrib["id"]) |
| 核心属性 | Element.text |
获取当前元素的文本内容(标签包裹的纯文本,如 <name>张三</name>的 “张三”) |
无文本则返回 None,需用 strip()去除首尾空格 |
关键区别在于:
- 递归 vs 非递归:
iter()是唯一能穿透所有层级的遍历工具,findall()/find()只查直接子级; - 返回形式:
iter()返回迭代器、findall()返回列表、find()返回单个元素; - 属性 vs 内容:
tag/attrib/get()操作「标签本身」,text操作「标签包裹的文本」;
基于我前面所提供的xml结构文件,我们此处再演示一下这些工具的具体用法。
Case1: Element.iter () —— 递归查找所有层级的指定元素
场景:想获取XML中所有的uniprot标签(不管它在哪个层级,是root节点的下一级子节点、还是递归的子节点)
# 递归遍历所以uniprot元素
for uniprot in root.iter("uniprot"):
print(f"Uniprot ID: {uniprot.text.strip()}")

IDEAL_entry本身没有文本内容,会返回None

我们想要的是直接的文本内容

展示所有子元素标签内容如下:
# 拓展:遍历所有IDEAL_entry下的所有子元素(不指定标签)
for elem in root.iter("IDEAL_entry"):
print(f"\nEntry标签: {elem.tag}, 属性: {elem.attrib}")
# 遍历当前entry下的所有子元素(递归,不指定标签)
for child in elem.iter():
if child.text and child.text.strip(): # 只打印有文本内容的元素
print(f" 子元素标签: {child.tag}, 内容: {child.text.strip()}")

root.iter()是找到所有子元素(不指定标签),所有层级都展示,其实打印全文。
root.iter(“uniprot”)会穿透IDEAL->IDEAL_entry->General层级,找到所有uniprot,这就是iter最核心的价值。
Case2:Element.findall()—— 只查直接子元素的指定标签
场景:想获取根节点下的所有直接子元素(直接子节点)<IDEAL_entry>。
# 查找root的直接子元素中所有IDEAL_entry
entries = root.findall("IDEAL_entry")
print(f"总共有 {len(entries)} 个 IDEAL_entry")
entries

# 查找root的直接子元素中所有IDEAL_entry
entries = root.findall("IDEAL_entry")
print(f"总共有 {len(entries)} 个 IDEAL_entry")
# 遍历每一个entry,查找其直接子元素General
for entry in entries[:5]: # 只看前5个条目
general = entry.findall("General")[0] # 获取第一个General元素, 或者用find
name = general.find("name").text.strip() # General的直接子元素name
print(f"Entry {entry.find('idp_id').text.strip()}: name = {name}")
# 易错点:findall 不递归!
# 错误示例:想从root直接findall('uniprot') → 找不到,因为uniprot不是root的直接子元素
wrong_uniprots = root.findall('uniprot')
print(f"root.findall('uniprot') 结果: {wrong_uniprots}") # 输出 []

root.findall(“IDEAL_entry”)能找到,因为IDEAL_entry是root的直接子元素;
但是root.findall(“uniprot”)找不到,因为uniprot在General下,不是直接子元素。
Case3:Element.find()—— 只查直接子元素的第一个指定标签
场景:想快速获取第一个<IDEAL_entry>的文本。
# 找到root下第一个IDEAL_entry
first_entry = root.find("IDEAL_entry")
# 找到first_entry下第一个General
general = first_entry.find("General")
# 找到General下第一个name
name = general.find("name").text.strip()
print(f"第一个Entry的名称: {name}")

正好对应上了:

然后关于查找的话,可以通过使用XPath,更精确地指定要查找的元素。
# 拓展:结合XPath精确查找
# 用find+XPath 直接找第1个IDEAL_entry 下的motif_name
motif_name = root.find("IDEAL_entry[1]/General/motif/motif_name")
print(f"第1个IDEAL_entry的motif_name: {motif_name.text.strip()}")

总而言之,find()是“找第一个”的快捷方式,适合我们确定目标元素只有1个的场景,另外结合XPath能大幅提升查找精确度,细节如下:
XPath语法
此处需要简单解释一下XPath的概念,XPath不是简单的相对路径,是XML专属的查询语言。
XPath(XML Path Language)是专门用于在 XML 文档中定位节点的查询语言,我们可以把它理解为:
- 像操作系统的「文件路径」(比如
C:/folder/file.txt),但功能更强; - 支持「绝对路径」「相对路径」「条件筛选」「位置筛选」等;
- 不是 Python 专属,是 XML 解析的通用标准(Java、JavaScript 解析 XML 也用 XPath)。
它和普通文件路径的核心区别在于:
| 特征 | 普通文件路径 | XPath |
|---|---|---|
| 层级穿透 | 只能写固定层级 | 支持 //穿透任意层级 |
| 位置筛选 | 无 | 支持 [1][last()] |
| 条件筛选 | 无 | 支持 [@id="001"] |
| 路径类型 | 绝对 / 相对 | 绝对 / 相对 / 递归 |
比如说前面的XPath路径就是“IDEAL_entry[1]/General/motif/motif_name”,
我们可以改成".//IDEAL_entry[1]/General/motif/motif_name",
效果还是:

首先是核心符号的含义
| 符号 | 作用 |
|---|---|
/ |
子节点分隔符(严格层级,只找直接子节点) |
// |
递归下降(穿透任意层级,找所有后代节点) |
. |
当前节点(根节点调用时,.//等价于 //,更规范) |
[n] |
位置筛选(取第 n 个匹配的节点,XPath 下标从 1 开始,不是 0) |
综上我们可以对该XPath解析表达式理解如下:
.//:从「当前节点(root)」开始,递归查找所有层级中符合条件的节点(穿透任意子级);IDEAL_entry[1]:找到所有IDEAL_entry节点中第 1 个(XPath 下标从 1 开始);/General:取这个IDEAL_entry的直接子节点General;/motif:取General的直接子节点motif;/motif_name:取motif的直接子节点motif_name。
前面我用的是直接写IDEAL_entry开头,为什么不用//呢?
两种写法在我的例子中都是可以的。
关键在于root是节点,IDEAL_entry是root的直接子节点,所以我直接写IDEAL_entry[1]本身能够找到;
但是如果我的XML结构变化,比如说IDEAL_entry被包在里,
比如说结构:
<IDEAL><data><IDEAL_entry>...</IDEAL_entry></data></IDEAL>
这个时候直接写IDEAL_entry[1]就找不到了,而.//IDEAL_entry[1]依然能够穿透找到。
我们可以举一个简单的例子来展示一下(结合.//,使用非根节点):
# 此处须使用相对路径,因为find是从当前节点开始查找的
name = root.find(".//General[1]/name")
print(f"第1个IDEAL_entry的General name: {name.text.strip()}")


结合我们前面的XML场景,整理一些最常用的XPath写法:
| 需求 | XPath 表达式 | 说明 |
|---|---|---|
| 递归找所有 uniprot | .//uniprot |
穿透所有层级找 uniprot |
| 找 root 直接子节点 version | version或 ./version |
./表示当前节点的直接子节点 |
| 找第一个 entry 的所有 synonym | .//IDEAL_entry[1]//synonym |
先找第一个 entry,再递归找 synonym |
| 找最后一个 entry 的 name | .//IDEAL_entry[last()]/General/name |
last()取最后一个匹配节点 |
# 1, 查找所有uniprot节点
uniprots = root.findall(".//uniprot")
print(f"共找到 {len(uniprots)} 个uniprot节点")
for i, uniprot in enumerate(uniprots[:5]): # 只看前5个
print(f"uniprot {i+1}: {uniprot.text.strip()}")

# 2, 查找所有version节点
versions = root.findall("version")
print(f"共找到 {len(versions)} 个version节点")
print(versions[0].text.strip() if versions else "未找到version节点")

# 3, 查找第1个IDEAL_entry下的所有synonym节点
synonyms = root.findall(".//IDEAL_entry[1]//synonym")
print(f"第1个IDEAL_entry的synonym节点数量: {len(synonyms)}")
for i, synonym in enumerate(synonyms[:5]): # 只看前5个
print(f"synonym {i+1}: {synonym.text.strip()}")

# 4, 查找最后一个IDEAL_entry下的General/name节点
name = root.find(".//IDEAL_entry[last()]/General/name")
print(f"最后一个IDEAL_entry的General name: {name.text.strip() if name is not None else '未找到'}")

带属性筛选的XPath写法
然后就是一些带属性筛选的写法,比较高频:
如果我们的XML有属性,比如说 <IDEAL_entry id=“IID00001”> ,那么就可以精确筛选:
# 找 id="IID00001" 的 entry 下的 name
entry_name = root.find('.//IDEAL_entry[@id="IID00001"]/General/name')
print(entry_name.text.strip()) # 输出对应名称
# 找所有 id 包含 "IID000" 的 entry(模糊匹配)
# 注:xml.etree 仅支持基础 XPath,模糊匹配需用 lxml 库
entries = root.findall('.//IDEAL_entry[contains(@id, "IID000")]')
当然我此处提供的xml文档中,IDEAL_entry并没有属性,只有子节点。
比如说idp_id,就是IDEAL_entry的子节点,并不是其属性。
如果我们按照节点的属性的查找语法去查找子节点,那么会报错
entry_name = root.find('.//IDEAL_entry[@idp_id="IID00001"]/General/name')
print(entry_name.text.strip())

这两个场景是不同的:
<!-- 场景1:idp_id是IDEAL_entry的【子节点】(我们的真实结构) -->
<IDEAL_entry>
<idp_id>IID00001</idp_id> <!-- 子节点:在IDEAL_entry内部,是独立的标签 -->
<General>...</General>
</IDEAL_entry>
<!-- 场景2:如果idp_id是IDEAL_entry的【属性】(我们前面的写法) -->
<IDEAL_entry idp_id="IID00001"> <!-- 属性:在IDEAL_entry标签内,是键值对 -->
<General>...</General>
</IDEAL_entry>
简单来说:匹配 XML 节点的**属性和子节点**是 XML 解析中最核心的两个概念,新手很容易混淆 。
| 维度 | 匹配属性 | 匹配子节点 |
|---|---|---|
| XML 结构特征 | 属性是节点标签内的键="值",属于节点的 “属性信息” |
子节点是节点内部的独立标签,属于节点的 “子元素” |
| XPath 语法 | 节点[@属性名="属性值"](带 @和方括号) |
节点[子节点名="子节点文本值"](无 @) |
| 我们前面的写法 | 用IDEAL_entry[@idp_id="IID00001"]找子节点 —— ❌ |
IDEAL_entry[idp_id="IID00001"]找子节点 —— ✅ |
| 访问值的方式 | 节点.get("属性名") |
子节点.text(先找到子节点) |
补充:如何快速判断1个键值对是属性还是子节点?
- 看位置:如果 “键值对” 写在
<节点名 ...>里面(标签的尖括号内),就是属性; - 看形式:如果是
<xxx>值</xxx>这种独立标签,且嵌套在父节点内部,就是子节点; - 代码验证:
- 想获取属性:
node.get("属性名")(返回字符串或 None); - 想获取子节点:
node.find("子节点名")(返回节点对象或 None)。
- 想获取属性:
简单总结:
- 语法核心区别:匹配属性用
[@属性名="值"](带@),匹配子节点用[子节点名="值"](无@); - 结构核心区别:属性是节点标签内的键值对,子节点是节点内部的独立标签;
- 前面示例的问题:把 “子节点 idp_id” 误判为 “属性 idp_id”,导致 XPath 语法错误,找不到节点。
记住这个核心:@ 是属性的专属标识,看到@就对应节点标签内的键值对,没@就对应子节点的文本值。
所以改过来之后是:
entry_name = root.find('.//IDEAL_entry[idp_id="IID00001"]/General/name')
print(entry_name.text.strip())

还有一些多条件筛选的写法
# 找第一个 entry 中 motif_region_start > 150 的 motif_name
# 注:xml.etree 不支持数值比较,需用 lxml 库
# 示例(lxml 写法):
from lxml import etree
tree = etree.fromstring(xml_content)
motif = tree.xpath('.//IDEAL_entry[1]//motif_region_start[text() > 150]/../motif_name')[0]
print(motif.text)
一些常用的XPath函数
| 函数 | 作用 | 示例 |
|---|---|---|
last() |
取最后一个节点 | .//IDEAL_entry[last()] |
position() |
取指定位置节点 | .//IDEAL_entry[position()<3] |
contains() |
模糊匹配属性 / 文本 | .//IDEAL_entry[contains(@id,"IID")] |
text() |
匹配节点文本 | .//name[text()="Test Protein"] |
另外基于我们前面的xml,一些高频的用法(形式上可借鉴,内容上没check过,可能报错)
# 示例1:提取所有 IDEAL_entry 的 id 和对应的 uniprot 列表
for entry in root.findall('.//IDEAL_entry'):
entry_id = entry.get('id')
# 递归找当前 entry 下的所有 uniprot
uniprots = [u.text.strip() for u in entry.findall('.//uniprot')]
print(f"Entry {entry_id}: Uniprot列表 = {uniprots}")
# 示例2:找所有有 motif 的 entry 名称
motif_entries = root.findall('.//IDEAL_entry[.//motif]') # 有motif子节点的entry
for entry in motif_entries:
name = entry.find('.//name').text.strip()
print(f"包含motif的Entry名称: {name}")
# 示例3:取第二个 entry 的第一个 uniprot
second_uniprot = root.find('.//IDEAL_entry[2]//uniprot[1]')
print(f"第二个Entry的第一个Uniprot: {second_uniprot.text.strip()}")
- XPath 核心:是 XML 节点查询语言,支持递归(
//)、精准层级(/)、位置筛选([1]/>last())、属性筛选([@key="value"]); - 关键符号:
//= 递归穿透所有层级,/= 仅直接子节点;[n]= 取第 n 个节点(XPath 下标从 1 开始);.//= 从当前节点递归查找(通用且容错);- 通用写法原则:
- 不确定层级 → 用
.//目标节点; - 确定层级 → 用
层级1/层级2/目标节点; - 需筛选 → 加
[条件](属性 / 位置 / 文本)。
XPath总结
- XPath 核心:是 XML 节点查询语言,支持递归(
//)、精准层级(/)、位置筛选([1]/last())、属性筛选([@key="value"]); - 关键符号:
//= 递归穿透所有层级,/= 仅直接子节点;[n]= 取第 n 个节点(XPath 下标从 1 开始);.//= 从当前节点递归查找(通用且容错);
- 通用写法原则:
- 不确定层级 → 用
.//目标节点; - 确定层级 → 用
层级1/层级2/目标节点; - 需筛选 → 加
[条件](属性 / 位置 / 文本),注意区分节点属性和子节点
- 不确定层级 → 用
XPath补充
参考官方xml部分补充的XPath内容:https://docs.python.org/zh-tw/3.11/library/xml.etree.elementtree.html#xpath-support

支持的一些XPath语法:

Case4:Element.get()—— 获取元素的属性值
场景:想获取<IDEAL_entry>下某个带有节点内键值对值的标签上的id属性;
节点的属性和子节点的键值对我们前面已经比较过了,本质上.get其实就是获取1个字典或者说键值对的一种防御性编程写法。
get() 是获取单个属性的最优方式,比直接取 attrib[‘id’] 更安全(不会因属性不存在报错)。
因为我们前面xml文本中并没有直接在节点内注明键值对的标签,所以例子略
Case5:tag/attrib/text属性的组合使用
场景:完整提取一个<IDEAL_entry>的所有关键信息
entry = root.find('IDEAL_entry')
print(f"元素标签名: {entry.tag}") # 输出 IDEAL_entry
print(f"元素所有属性: {entry.attrib}") # 输出 {}
print(f"元素id属性: {entry.get('id')}") # 输出 None
# 提取子元素文本
general = entry.find('General')
name = general.find('name').text.strip() # 提取name文本
synonym = general.find('synonym').text.strip() # 提取synonym文本
print(f"名称: {name}")
print(f"别名: {synonym}")

简单来说:
- 遍历 / 查找核心区别:
iter()递归查所有层级,findall()/find()只查直接子级; - 属性 vs 内容:
tag/attrib/get()操作标签本身的属性 / 名称,ext操作标签包裹的文本; - 使用场景:
- 想找所有层级的某类元素 → 用
iter(); - 想精准找直接子元素的某类元素(多个)→ 用
findall(), 用XPath可以突破层级限制; - 想找直接子元素的第一个目标元素 → 用
find(); - 想获取元素属性 → 用
get()(安全)或attrib(字典); - 想获取元素文本 → 用
text(记得strip()去空格)
实例分析:我的一个例子
xml文档还是前面那个,背景如下:
我有1个数据库(官方链接:https://www.ideal-db.org/)需要挖掘,但是官方并没有直接提供REST API之类接口,所以我们没法直接下载;

除了爬虫爬取之外,官方还提供了一个直接的打包压缩的xml格式数据,
鉴于前后两次获取数据的时间比较长,发现数据库又更新了,所以现在手头上有两个xml格式数据,现在想要解析之后相互比较一下,看选哪一个,然后深入解析,获取想要的数据。
- 目的1:对于每一个数据库xml,需要解析的内容,每一个entry条目要获取其 accession号、关联的uniprot序列号(可能有多个)、disorder无序区域(起止坐标)
- 目的2:比较这两个新旧数据库xml之间上述内容的异同,节点键值对的异同,相同的部分就不需要修改代码逻辑,不同的部分需要修改
import xml.etree.ElementTree as ET
tree_new = ET.parse("IDEAL_20250908_02Mar2026.xml") # 更新之后的版本内容
tree_old = ET.parse("IDEAL_17Apr24.xml") # 更新之前的版本内容
root_new = tree_new.getroot()
root_old = tree_old.getroot()
部分内容展示如下:

其中XML格式官网组织参考如下:https://www.ideal-db.org/chart.html(该XML格式内容对应最新版本的数据库组织,对旧版本的数据不一定兼容)

我们先来看一下这两个xml格式内容数据上的一些区别(键值对展开),先看相同idp_id的那些条目entry:
# 遍历所有 IDEAL_entry: root_old
entry = root_old.find('IDEAL_entry[idp_id="IID00001"]') # 只看 idp_id 为 IID00001 的条目
print(f"=== IDEAL_entry old 开始 ===")
# 递归遍历 entry 下的所有子节点(包括子子节点)
elem_count = 0
for elem in entry.iter('*'):
if elem.text and elem.text.strip(): # 只打印有文本内容的节点
print(f"{elem.tag}: {elem.text.strip()}")
elem_count += 1
print(f"=== IDEAL_entry old 结束 (共 {elem_count} 个元素) ===\n")

# 遍历所有 IDEAL_entry: root_new
entry = root_new.find('IDEAL_entry[idp_id="IID00001"]') # 只看 idp_id 为 IID00001 的条目
print(f"=== IDEAL_entry new 开始 ===")
# 递归遍历 entry 下的所有子节点(包括子子节点)
elem_count = 0
for elem in entry.iter('*'):
if elem.text and elem.text.strip(): # 只打印有文本内容的节点
print(f"{elem.tag}: {elem.text.strip()}")
elem_count += 1
print(f"=== IDEAL_entry new 结束 (共 {elem_count} 个元素) ===\n")

很直观地,我们能够看到即使是对于同一条目,数据库也会进行更新,可能是对附属的子节点的键值对进行增删修改之类。
我们暂时以idp_id为"IID00001"的IDEAL_entry为例,进行模板解析展示。
对照官网的最新xml数据组织表,
- accession即idp_id,直接在每一个条目的直接子节点下,IDEAL_entry/idp_id

- uniprot id即IDEAL_entry/General/uniprot,不在每个条目的直接子节点下,我们可以用XPath语法

- disorder区域注释:我们需要找到每一个条目标注的蛋白质无序区域的起始与终点坐标:
但是这个区域的注释就有很多可疑选项了- IDEAL_entry/Region/order_disorder

- IDEAL_entry/Function_pros/disorder_location

- 其他预测器部分: AlphaFold, NeProc, SEG等
我们先按照固定的entry,展示同1个entry的这3个区域来比较看看:
首先是Region这个IDEAL_entry下的直接子类,我们可以看到其实这个子节点不像General一样,General是每一个IDEAL_entry只有1个子节点,但是Region有很多个子节点,所以我们需要findall;
而且我们可以发现,只有order_disorder属性为disorder的Region子节点才是我们需要的子节点,然后提取这个子节点的region_start以及region_end值


经过比对发现,Region的结构组成格式在新数据库以及旧数据库中是格式不变的,我们的代码逻辑可以一致:
entry = root_old.find('IDEAL_entry[idp_id="IID00001"]') # 只看 idp_id 为 IID00001 的条目
idr_count = 0
for regions in entry.findall(".//Region"):
this_region_order_disorder_status = regions.find("order_disorder").text
this_region_start = regions.find("region_start").text
this_region_end = regions.find("region_end").text
if this_region_order_disorder_status == "disorder":
print(f"Region {this_region_start}-{this_region_end} is disordered.")
idr_count += 1
print(f"Total IDRs in this entry: {idr_count}")
从结果上来看,光光是IID00001这个entry就有51个无序disorder region

当然,我们也可以直接依据节点的order_disorder子节点内容筛选目标节点,没必要每一个节点的内容都解析出来再判断:
也就是下面这种写法,直接用Region[order_disorder=‘disorder’]键值对框定目标节点
entry = root_old.find('IDEAL_entry[idp_id="IID00001"]') # 只看 idp_id 为 IID00001 的条目
idr_count = 0
for regions in entry.findall(".//Region[order_disorder='disorder']"):
this_region_start = regions.find("region_start").text
this_region_end = regions.find("region_end").text
print(f"Region {this_region_start}-{this_region_end} is disordered.")
idr_count += 1
print(f"Total IDRs in this entry: {idr_count}")
我们接着看更新之后的数据库的该条目:代码逻辑一样
entry = root_new.find('IDEAL_entry[idp_id="IID00001"]') # 只看 idp_id 为 IID00001 的条目
idr_count = 0
for regions in entry.findall(".//Region[order_disorder='disorder']"):
this_region_start = regions.find("region_start").text
this_region_end = regions.find("region_end").text
print(f"Region {this_region_start}-{this_region_end} is disordered.")
idr_count += 1
print(f"Total IDRs in this entry: {idr_count}")
依然是51个条目,

我们接着再看IDEAL_entry/Function_pros/disorder_location这个条目的信息;
首先是旧数据库:
我们可以明显看到,没有disorder的region,这和前面的Region节点获取的信息完全不一致!
而且旧的数据库根本没有Function_pros这个节点!

然后是更新之后的数据库:
新的数据库中的Function_pros节点是对应旧数据库中的Function节点的,而且直接就没有disorder_location这个子条目了!

从这个例子可以看到,Function/Function_pros是新旧数据库之间对应的条目,但是这个条目无论是新旧数据库,都没有合适地描述出来,至少这个条目中认为是没有无序区域注释的。
然后就是其余预测器预测的无序区域部分,这一部分在新的数据库中是有的,在旧数据库中是不兼容的

在新数据库中就在Function_pros的下一个同级节点中,也是每一个IDEAL_entry的末尾部分。
对于这个数据,我们其实是完全用不上的,因为我们的目的是为了获取真实的无序区域坐标数据,而不是说预测结果。
综上所述,能用的结果只有IDEAL_entry/(Function_pros/Function)/disorder_location条目,以及 IDEAL_entry/Region/order_disorder条目。
但是显然后者提供的信息比前者多。
为了能够更加公正公平地做出决策,我们用实际数据库中的注释,结合经验来判断。
还是用IID00001这个entry为例,
这是新数据库中的Region条目:

比如说我们随便拿第2个条目335-337为例,是disorder的

我们可以多关注一下附加的信息:
chain id是1yokA,是PDB相关的;
然后condition id是1,我们先看看condition本身是什么样的条目:
可以看到condition应该是获取结构方面实验无序区域来源证据的条目


下面是新数据库中的Pros相关条目:

是没有区域范围信息的。
我们可以先看看条目,关键在于这里的pros怎么理解,是否可能是因为pros的定义所以决定其注释在同一个entry内有所不同?

参考官网注释:https://www.ideal-db.org/

所以Pros其实是disorder-to-order也就是执行DOR的IDR片段,也就是说是特殊的无序区域,是无序区域IDR的一个子集。

我们再看看数据库里关于这个条目是怎么存储的:
https://www.ideal-db.org//ideal.php?id=IID00001

这个注释就很奇怪了,有least rule以及majority rule,看来是注释之间有矛盾,所以有少数以及多数之分。
然后我们前面的注释是335-337,可以看到,这一段区域在majority注释中是被标注为order的。
同时我们观察到least rule中有disorder和conflict重合的部分,大概是disorder和order重合了。
另外我们注意到pros和order、disorder的注释是并行的,虽然从定义上来讲pros是属于IDR区域的(当然这个其实也很容易搞混,DOR的区域在转换前是无序的,在转换后是有序的,那是不是按照least rule是conflict的?那是不是conflict的我们可以拿来作为候选的pros?但是其实不是,我们前面看到了在conflict的注释和pros又是并行的,而且相互之间没有重叠,而且pros的条目看细节是order_location兼disorder_location的,又是混杂的数据)。
那问题来了,我们为什么不获取无序区域全集,而是要获取1个特殊的无序区域子集呢?
这个其实就看自己的研究目的了。
如果我们的研究目的是“大网捞鱼”,也就是不错放任何一个,那么我们就可以直接获取所有的区域注释,
把真正的过滤、筛选步骤放在后面。
也就是说,我们在挖掘数据库方面是coarse-grained的,根本目的其实不是为了直接敲定精细的 IDR 边界,而是为了获取具有无序潜力的accession号(比如说有这个区域注释的entry号)。真正精细的下游在后面。
所以,为什么在前面数据库挖掘中混入了大量的噪音,但最终不会影响结果——因为在这段不折叠序列如果根本没有对应的 PDB 结构,或者在 PDB 结构中并没有其他链与它结合,后续的三维结构距离计算/过滤步骤会直接将其剔除。
也就是有的放矢,轻重缓急,也就是不同步骤调整战略。
也就是上游遵循“大网捞鱼”的广义搜索,最大化我们的初始搜索空间,防止漏掉候选数据,也就是说第一步其实是在胡乱捞数据,只求覆盖面广;然后下游我们会一一剔除。
所以,策略最终还是看全局处理流程定的:
- 算力足的话,我们取region列作为候选数据,务求覆盖面广
- 算力不足,或者我们的目的是为了获取高置信度、多个证据来源限制的region,我们取function中的pros
- 当然,在写脚本的时候,只要输出能够统一,其实我们完全可以两个都要,只不过是在参数中加一个脚本罢了
那么还是前面那个例子:
我就可以写一个大致的脚本如下,
首先是Region条目中筛选的,我要看看每一个条目的无序区域范围有哪些,以及总共有多少个条目
# IDEAL_entry/Region[order_disorder='disorder']/
entry_count = 0
for entry in root_new.findall("IDEAL_entry"):
idp_id = entry.find("idp_id").text
uniprot_id = ",".join([uniprot.text for uniprot in entry.findall("General/uniprot")])
regions = []
for region in entry.findall(".//Region[order_disorder='disorder']"):
if region.find("region_start") is None or region.find("region_end") is None:
continue
this_region_start = region.find("region_start").text
this_region_end = region.find("region_end").text
this_region = f"{this_region_start}-{this_region_end}"
regions.append(this_region)
if regions:
print("*"*20)
print(f"IDP ID: {idp_id}")
print(f"Uniprot ID: {uniprot_id}")
print(f"Disorder Regions: {",".join(regions)}")
entry_count += 1
print(f"Total entries with disorder regions: {entry_count}")

可以看到,总共是1206个条目

参考:https://www.ideal-db.org//history.html

其实可以看到,每一个条目如果都算1个uniprot,就算有交集,其实这个量级也差不多是全部的数据库条目了。
但是这里我们注意到一个问题,就是不同的region坐标之间有交集,所以这个坐标该如何处理?
我们可以取并集,或取交集,因为坐标到后面肯定是唯一的;所以这一块我们可以再加一个参数位。
为什么会有这么多重叠和碎片化的区段?
在 IDEAL 数据库中,每一个 <Region type="disorder"> 通常代表某一次特定结构实验(通常是某个具体的 PDB 结构)中缺失的电子密度区域。
- 同一个蛋白质(Uniprot ID)可能被不同的实验室解析了多次(对应多个 PDB ID)。
- 在不同的结晶条件、不同的配体结合状态或不同的序列截短下,这根多肽链表现出的“无序/柔性”边界会有细微变化。
- 比如对于 IID00005,某篇文献里 109-111 没解析出来,另一篇里 108-111 没解析出来,这就生成了两条独立的 Region 记录。
我们的做法是取并集(合并重叠或相邻的区间)
- 取并集(Union): 只要在某一次实验中被观察到是高度柔性/无序的,我们就认为该序列片段具有内在无序潜能。这也是几乎所有无序数据库(如 MobiDB)合成一致性特征(Consensus)的默认方式。
- 取交集将会丢失大量真实数据: 如果取交集,意味着只有在所有实验中都无法被解析的区域才算 IDR。但如果某个 IDP 在某次实验中恰好结合了抗体或者配体被诱导折叠(变成了有序结构),取交集就会把这根真实的 IDR 完全抹杀。
然后另外一种就是直接从disorder location中获取,这个就比较简单了,
# IDEAL_entry/Function_pros/disorder_location
entry_count = 0
for entry in root_new.findall("IDEAL_entry"):
idp_id = entry.find("idp_id").text
uniprot_id = ",".join([uniprot.text for uniprot in entry.findall("General/uniprot")])
regions = []
for pros in entry.findall(".//Function_pros[pros_type='verified']/disorder_location"):
if pros.find("disorder_region_start") is None or pros.find("disorder_region_end") is None:
continue
this_region_start = pros.find("disorder_region_start").text
this_region_end = pros.find("disorder_region_end").text
this_region = f"{this_region_start}-{this_region_end}"
regions.append(this_region)
if regions:
print("*"*20)
print(f"IDP ID: {idp_id}")
print(f"Uniprot ID: {uniprot_id}")
print(f"Disorder Regions: {",".join(regions)}")
entry_count += 1
print(f"Total entries with verified disorder_location: {entry_count}")


这个子集就比较少了。
另外,我们同样注意到,这个区域之间是有交集的
更多推荐


所有评论(0)