3、元数据基础格式:JSON、YAML、XML 的结构与 Python 解析

做样本元数据管道,说白了就是要跟一堆元数据文件打交道。我这些年见过不少项目,一开始都拍脑袋选格式,结果中途不得不来回转换,浪费大量时间。这一章,我把三个最主流的元数据格式梳理清楚,顺便讲讲 Python 里怎么解析它们。

核心结论:大多数样本元数据场景下,我推荐 JSON 或 YAML。XML 虽然强大,但在描述性元数据里过于啰嗦。但现实是,很多上游工具还在输出 XML,你躲不掉。

元数据基础格式对比图 JSON ● 轻量文本格式 ● 原生支持字典/列表 ● 标准库 json 模块 YAML ● 人类可读性最强 ● 缩进敏感 ● 需要 PyYAML/ruamel.yaml XML ● 标签/属性双通道 ● 支持 Schema 校验 ● ElementTree / lxml 推荐优先级:YAML (配置) > JSON (数据交换) > XML (遗留系统)

3.1 JSON —— 最通用的数据交换格式

JSON 在我眼里就是做数据管道的候选默认格式。Python 的 json 标准库已经非常完善,解析效率比 YAML 高不少。曾经有个项目,元数据文件有几十万个,我连夜把 YAML 改成了 JSON,解析时间从 3 秒降到了 0.4 秒。

JSON 的结构很简单:对象(字典)和数组(列表)相互嵌套。键名用双引号,字符串也是双引号。

{
  "sample_id": "SAMPLE-001",
  "protocol": "RNA-seq",
  "sequencer": "NovaSeq 6000",
  "read_length": 150,
  "index": {
    "i5": "ATCACG",
    "i7": "CGTGAT"
  },
  "tags": ["fastq", "paired-end"]
}

Python 解析就两行:

import json

with open("metadata.json", "r") as f:
    data = json.load(f)

print(data["sample_id"])  # SAMPLE-001

小技巧:我习惯在写入 JSON 时用 ensure_ascii=False 和 indent=2,这样中文元数据不转义,阅读起来舒服。

with open("output.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

3.2 YAML —— 人类可读性的天花板

YAML 是另一个极端。它用缩进代替括号和引号,肉眼看着跟伪代码一样。我刚开始用的时候也有点不习惯,觉得缩进控制很麻烦。但后来我发现,在写样本元数据的人工配置模板时,YAML 几乎无敌。

你看同一个元数据用 YAML 写出来:

sample_id: SAMPLE-001
protocol: RNA-seq
sequencer: NovaSeq 6000
read_length: 150
index:
  i5: ATCACG
  i7: CGTGAT
tags:
  - fastq
  - paired-end

是不是一目了然?没有引号、没有大括号,直接就能读。

Python 解析 YAML 需要第三方的 PyYAML 或 ruamel.yaml:

import yaml

with open("metadata.yaml", "r") as f:
    data = yaml.safe_load(f)

print(data["sample_id"])  # SAMPLE-001

踩坑提醒:我曾因为用 yaml.load() 而不是 safe_load() 导致生产环境被恶意代码注入——YAML 支持任意 Python 对象反序列化,safe_load() 是安全红线,永远别省。

3.3 XML —— 老大哥的遗产

XML 在我看来就像是元数据格式里的化石。它的表达能力确实强,既有元素内容又有属性,还能用命名空间搞分层。但写起来实在太长了。我们看看同样的元数据:

<?xml version="1.0" encoding="UTF-8"?>
<sample>
  <id>SAMPLE-001</id>
  <protocol>RNA-seq</protocol>
  <sequencer>NovaSeq 6000</sequencer>
  <read_length>150</read_length>
  <index i5="ATCACG" i7="CGTGAT"/>
  <tags>
    <tag>fastq</tag>
    <tag>paired-end</tag>
  </tags>
</sample>

Python 标准库提供了 xml.etree.ElementTree:

import xml.etree.ElementTree as ET

tree = ET.parse("metadata.xml")
root = tree.getroot()

sample_id = root.find("id").text
read_length = int(root.find("read_length").text)
print(sample_id, read_length)

但你如果想解析大型的 XML(比如几百 MB 的 SRA 元数据),就要考虑 lxml,它支持 XPath 和流式解析:

from lxml import etree

tree = etree.parse("metadata.xml")
# XPath 查找
for elem in tree.xpath("//sample[sequencer='NovaSeq 6000']"):
    print(elem.find("id").text)

3.4 三种格式的核心差异对比

我做了一个对比表,方便你一眼看出区别:

特性 JSON YAML XML
语法简洁度 中等 高(缩进) 低(标签冗余)
数据类型支持 原生:str/int/float/list/dict/None 原生,额外支持日期/集合等 纯文本,需手动转换
解析速度(1K 文件) ~0.2ms ~1.5ms ~2ms (ElementTree) / ~1ms (lxml)
标准库支持 标准库 json 需第三方 标准库 ElementTree
Schema 校验 jsonschema (第三方) 无官方 Schema XSD (正式标准)
生物信息通用度 高(API 返回、样本清单) 中(配置模板) 高(SRA 元数据、Bioproject)

3.5 Python 解析参数对照表

实际开发中,你总会遇到需要从一种格式转换到另一种的情况。这是我整理的关键参数:

操作 JSON YAML XML
读取文件 json.load(f) yaml.safe_load(f) ET.parse(f).getroot()
字符串解 json.loads(s) yaml.safe_load(s) ET.fromstring(s)
写入文件 json.dump(d, f) yaml.dump(d, f) tree.write(f)
格式化输出 indent=2 default_flow_style=False pretty_print=True (lxml)
大文件流式 需 ijson 需 yaml.load_all 需 lxml.iterparse

个人经验:在样本元数据管道中,我一般用 YAML 写模板定义文件(给人类编辑),用 JSON 做中间交换格式(给程序处理),XML 只在对接 NCBI ENA 这类外部系统时才碰。

3.6 避坑指南

  • 编码问题:JSON 默认只支持 Unicode,但旧系统可能输出 GBK 或 Latin-1 编码的 XML。我曾经遇到一个 ENA 的 XML 文件,根元素声明编码是 utf-8,实际内容是 Latin-1,解析直接崩了。解决方案是用 charset-normalizer 先探测编码。
  • YAML 的布尔值陷阱:YAML 会把 yes、true、on 都解释为 True,no、false、off 都解释为 False。如果你的样本 ID 恰好叫 "no",你想想看会发生什么……用引号包住字符串就可以避免。
  • XML 命名空间:解析 NCBI 的 XML 时,大量元素都带着命名空间前缀(如 ns0:Sample)。直接 find("Sample") 是找不到的,必须用 find("{命名空间}Sample") 或者 findall(".//*[local-name()='Sample']")。