数据解析:XML与JSON格式的处理技巧

做PubMed数据挖掘,你绕不开一个现实问题——数据到底长什么样?

PubMed官方给的是XML,结构严谨但啰嗦。民间工具和API爱用JSON,轻便但信息密度高。这两者怎么切换、怎么解析、怎么避免踩坑,是本章要解决的核心。

我个人习惯是:拿XML做数据溯源,用JSON做日常开发。两种格式各有脾气,摸透了就顺手了。

1. PubMed数据格式概览

PubMed的原始数据是XML,每条记录就像一个嵌套的树。你想想看,一篇文献的作者、标题、摘要、MeSH词、药物相关字段,全都挂在不同的节点下。

我曾经在项目里直接用正则去抓XML里的文本,结果被嵌套标签坑惨了。后来老老实实用解析器,效率反而更高。

下面这张图展示了从PubMed原始XML到JSON转换的核心逻辑:

PubMed数据解析与格式转换流程 PubMed XML 原始数据 XML解析引擎 结构化数据模型 JSON 输出 Python Dict 数据库/API 核心思路:统一入口 → 灵活输出 XML负责数据完整性,JSON负责交互效率 开发阶段优先使用JSON调试,数据审计阶段使用XML溯源

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}")
命名空间陷阱:PubMed XML有默认命名空间,直接用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. 本章核心表格

两种格式的特点和使用场景,我整理成了一张表:

特性XMLJSON
数据完整度高,保留全部元数据中,需要自定义提取
解析速度慢(节点多)快(结构扁平)
嵌套深度深,树状浅,字典/列表
调试友好度低,标签多高,结构直观
推荐使用场景数据存储、审计开发调试、API交互
给我的习惯:写爬虫时先用JSON验证逻辑,交付时再输出XML原文件做备份。两种格式其实就是一件事的两面——你要是只吃甜豆腐脑,错过咸的也挺可惜的。