数据解析:XML与JSON格式的处理技巧
做PubMed数据挖掘,你绕不开一个现实问题——数据到底长什么样?
PubMed官方给的是XML,结构严谨但啰嗦。民间工具和API爱用JSON,轻便但信息密度高。这两者怎么切换、怎么解析、怎么避免踩坑,是本章要解决的核心。
我个人习惯是:拿XML做数据溯源,用JSON做日常开发。两种格式各有脾气,摸透了就顺手了。
1. PubMed数据格式概览
PubMed的原始数据是XML,每条记录就像一个嵌套的树。你想想看,一篇文献的作者、标题、摘要、MeSH词、药物相关字段,全都挂在不同的节点下。
我曾经在项目里直接用正则去抓XML里的文本,结果被嵌套标签坑惨了。后来老老实实用解析器,效率反而更高。
下面这张图展示了从PubMed原始XML到JSON转换的核心逻辑:
2. 用Python解析XML数据
Python标准库里的xml.etree.ElementTree就能搞定大部分工作。我不喜欢装额外的第三方库,除非数据量巨大到必须用lxml。
2.1 基础解析流程
加载XML、定位根节点、遍历子节点——三步走。
import xml.etree.ElementTree as ET
tree = ET.parse('pubmed_result.xml')
root = tree.getroot()
# 遍历所有文章 (PubmedArticle)
for article in root.iter('PubmedArticle'):
pmid = article.find('.//PMID').text
title = article.find('.//ArticleTitle').text
print(f"PMID: {pmid}, Title: {title}")
这里要注意:find只返回第一个匹配的节点,findall返回所有。
2.2 用XPath精准定位
数据层级深了,别用多层find嵌套。我习惯直接用XPath路径表达式。
| XPath表达式 | 含义 |
|---|---|
.//MeshHeading | 查找所有层级的MeshHeading节点 |
./Author/LastName | 当前节点下的作者姓氏 |
./*[local-name()='Chemical'] | 忽略命名空间,匹配Chemical节点 |
举一个提取化学物质信息的例子:
for article in root.iter('PubmedArticle'):
chemicals = article.findall('.//Chemical')
for chem in chemicals:
name = chem.find('.//NameOfSubstance').text
registry = chem.find('.//RegistryNumber').text
print(f"名称: {name}, 注册号: {registry}")
find可能返回None。我刚开始用XPath时,经常在命名空间上栽跟头。建议先打印根节点的tag确认:print(root.tag),里面那个大括号内的uri就是命名空间。3. 命名空间处理
我记得第一次爬PubMed数据,写了半天代码,结果全是空列表。一查,原来命名空间在作怪。
解法有两种:
- 注册命名空间别名,用
ns:tag格式访问 - 用
{uri}tag完整写法
我个人推荐第一种,代码可读性更好:
ns = {'ns': 'http://www.ncbi.nlm.nih.gov/.../pubmed'}
for article in root.iter('ns:PubmedArticle'):
title = article.find('ns:MedlineCitation/ns:Article/ns:ArticleTitle', ns)
if title is not None:
print(title.text)
root.findall('.//{http://...}ArticleTitle')绕过命名空间注册,但不适合有多个不同命名空间的情况。4. JSON格式的转换与交互
XML适合存,JSON适合传。搞定XML解析后,下一步往往是转成JSON,方便下游应用使用。
4.1 定制化转JSON
别偷懒直接用json.dumps()全量转。我习惯先提取关键字段,组织成字典,再序列化。
def article_to_dict(article):
return {
'pmid': article.find('.//PMID').text,
'title': article.find('.//ArticleTitle').text,
'abstract': article.find('.//AbstractText').text if article.find('.//AbstractText') is not None else '',
'chemicals': [
{
'name': chem.find('.//NameOfSubstance').text,
'registry': chem.find('.//RegistryNumber').text
}
for chem in article.findall('.//Chemical')
],
'mesh_terms': [
mesh.find('.//DescriptorName').text
for mesh in article.findall('.//MeshHeading')
if mesh.find('.//DescriptorName') is not None
]
}
articles = [article_to_dict(a) for a in root.iter('PubmedArticle')]
import json
print(json.dumps(articles[:2], indent=2, ensure_ascii=False))
4.2 JSON的优势场景
一旦转成JSON,数据结构就变平坦了。你可以轻松做数据分析、存MongoDB、或者给前端做可视化。
我习惯在开发阶段先用JSON交互,查找到具体问题再回XML做详细排查。
5. 批量处理的权衡
一次爬几百上千条PubMed记录,逐条解析会慢。这时候要批量加载、批量解析。
我曾经处理过30万条PubMed记录,用lxml的iterparse边读边解析,内存占用从2GB降到200MB。
import xml.etree.ElementTree as ET
context = ET.iterparse('large_pubmed.xml', events=('end',))
for event, elem in context:
if elem.tag.endswith('PubmedArticle'):
# 处理单篇文章
pmid = elem.find('.//PMID').text
# 处理完后清理
elem.clear()
# 可选:清理父节点
while elem.getprevious() is not None:
del elem.getparent()[0]
iterparse配合elem.clear(),是处理超大型XML的有效策略。你想想看,如果不清理,整个树都放在内存里,你的笔记本会哭的。6. 异常处理与日志
实战中,总有些记录格式不对。
- 有些文章没有摘要
- 有些化学物质名称字段为空
- 个别记录标签闭合异常
我的做法是:在解析代码里包一层try/except,记录错误日志。别让一条坏数据拖垮整个流程。
for article in root.iter('PubmedArticle'):
try:
pmid = article.find('.//PMID').text
title = article.find('.//ArticleTitle').text
abstract = article.find('.//AbstractText').text or ''
except Exception as e:
print(f"解析出错: {pmid}, 错误: {e}")
continue # 跳过这条,继续下一条
7. 本章核心表格
两种格式的特点和使用场景,我整理成了一张表:
| 特性 | XML | JSON |
|---|---|---|
| 数据完整度 | 高,保留全部元数据 | 中,需要自定义提取 |
| 解析速度 | 慢(节点多) | 快(结构扁平) |
| 嵌套深度 | 深,树状 | 浅,字典/列表 |
| 调试友好度 | 低,标签多 | 高,结构直观 |
| 推荐使用场景 | 数据存储、审计 | 开发调试、API交互 |