3、元数据基础格式:JSON、YAML、XML 的结构与 Python 解析
做样本元数据管道,说白了就是要跟一堆元数据文件打交道。我这些年见过不少项目,一开始都拍脑袋选格式,结果中途不得不来回转换,浪费大量时间。这一章,我把三个最主流的元数据格式梳理清楚,顺便讲讲 Python 里怎么解析它们。
核心结论:大多数样本元数据场景下,我推荐 JSON 或 YAML。XML 虽然强大,但在描述性元数据里过于啰嗦。但现实是,很多上游工具还在输出 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']")。