第3章 VCF文件解析:Python库与自定义解析
VCF文件解析,说白了就是把那些用制表符分隔的文本行,变成程序能直接操作的数据结构。我刚开始做变异数据处理时,第一反应就是找个现成的库——谁愿意自己造轮子呢?但跑过几个项目后发现,不同场景下的解析需求真不一样。有的要速度,有的要兼容,有的要能解析畸形数据。这一章我就把常用的解析手段摊开来讲,帮你选对工具。
3.1 主流Python库横向对比
Python生态里解析VCF的库不少,但真正经得起生产环境考验的,也就那么几个。我个人习惯在正式项目里首选cyvcf2,原因很简单——它基于htslib的C语言底层,解析速度是纯Python实现的10倍以上。你想想看,一个全基因组VCF文件动辄几十GB,解析速度直接决定了你的开发迭代效率。
下面这张表我整理了几个主流库的核心差异:
| 库名 | 解析速度 | 功能完整性 | 维护状态 | 推荐场景 |
|---|---|---|---|---|
| PyVCF | 较慢 | 完整 | 维护中 | 小型数据、教学验证 |
| cyvcf2 | 极快 | 完整 | 活跃 | 大规模生产环境 |
| vcfpy | 中等 | 较完整 | 维护中 | 兼容性优先的场景 |
选库的时候有一个容易被忽略的点:PyVCF对VCF 4.2的INFO字段解析更细致,但遇到超大文件会明显卡顿。cyvcf2则相反,它在读取时做了惰性加载,内存占用控制得很好。我在项目中遇到过两次因为用了PyVCF导致内存溢出的情况,后来全切到了cyvcf2。
3.2 cyvcf2实战:高效解析VCF
cyvcf2的安装很简单,一行命令就行。但要注意,它依赖htslib,在macOS上可能需要先装libomp:
pip install cyvcf2
基础用法几乎是直觉式的。打开一个VCF文件,遍历每个变异记录,直接访问属性:
from cyvcf2 import VCF
vcf = VCF("input.vcf.gz")
for variant in vcf:
chrom = variant.CHROM
pos = variant.POS
ref = variant.REF
alt = ",".join(variant.ALT)
qual = variant.QUAL
print(f"{chrom}\t{pos}\t{ref}\t{alt}\t{qual}")
小提示: cyvcf2 的 variant.ALT 返回的是一个元组,多个ALT等位基因时需要手动拼接。我一般直接用 ",".join(variant.ALT) 转成字符串,写入DataFrame时方便很多。
cyvcf2还支持按区域查询,这个功能在大文件里特别实用。比如你只想看1号染色体前1Mb的变异:
for variant in vcf("1:1-1000000"):
print(variant.CHROM, variant.POS, variant.REF, variant.ALT)
注意: 按区域查询要求VCF文件必须经过 bgzip 压缩并用 tabix 建立索引。缺少索引时cyvcf2会抛出 IndexError,而且提示信息不太明显。我第一次用的时候就卡在这里了,后来才发现是忘了跑 tabix -p vcf input.vcf.gz。
3.3 自定义解析器:何时需要,如何实现
现成库大部分场景够用,但总有些边界情况让你想自己动手。举几个例子:VCF文件里的FILTER字段有自定义标签,库的解析逻辑会直接丢弃未知标签;或者你需要把某个INFO子字段拆成多列,库的抽象层反而碍事。
我曾经在项目中遇到过一个坑:上游工具生成的VCF里,QUAL字段在某些行是缺失的,cyvcf2默认返回None,但下游分析流程期望的是字符串"NA"。为了一个字段改整个下游不现实,这时候自己写一个解析器反而是最干净的方案。
核心逻辑其实就几行:
def parse_vcf_line(line):
if line.startswith("#"):
return None
fields = line.strip().split("\t")
if len(fields) < 8:
return None
info_dict = {}
info_fields = fields[7].split(";")
for item in info_fields:
if "=" in item:
key, val = item.split("=", 1)
info_dict[key] = val
else:
info_dict[item] = True
return {
"chrom": fields[0],
"pos": int(fields[1]),
"id": fields[2],
"ref": fields[3],
"alt": fields[4],
"qual": fields[5] if fields[5] != "." else "NA",
"filter": fields[6],
"info": info_dict,
}
这个函数保留了原始字段的完整性,QUAL为缺失值时返回"NA",INFO字段也拆成了字典。关键是你可以在里面自由加逻辑,遇到不合规的行直接返回None,不会让整个解析崩溃。
个人经验: 自定义解析器的单元测试一定要写全。我一般会准备三组测试数据:一行标准格式、一行缺失QUAL、一行INFO为空。这三行覆盖了大部分线上异常场景。
3.4 解析结果结构化:从VCF到DataFrame
解析完VCF,下一步就是把它变成结构化数据。我个人习惯统一输出为Pandas DataFrame,因为后续的过滤、统计、知识图谱构建都能直接复用。用cyvcf2配合DataFrame,代码非常紧凑:
import pandas as pd
from cyvcf2 import VCF
vcf = VCF("input.vcf.gz")
rows = []
for variant in vcf:
rows.append({
"chrom": variant.CHROM,
"pos": variant.POS,
"ref": variant.REF,
"alt": ",".join(variant.ALT),
"qual": variant.QUAL,
"filter": variant.FILTER,
})
df = pd.DataFrame(rows)
print(df.head())
如果你需要把结果导出成JSON供下游API使用,直接调用 df.to_json() 即可。但我建议按变异记录拆成列表,方便逐条写入知识图谱:
records = df.to_dict(orient="records")
import json
with open("variants.json", "w") as f:
json.dump(records, f, indent=2)
核心思路: 解析阶段的输出格式决定了后续整合的复杂度。我强烈建议统一使用"记录列表"结构——每条变异是一个字典,字段名与VCF标准保持一致。这样不管你是要构建知识图谱还是做统计分析,接口都是统一的。
3.5 本章知识体系
下面这张图把VCF解析的整体脉络串了起来。从源文件开始,到选择解析策略,再到最终的结构化输出和知识图谱构建,每一步都是承上启下的。你可以在自己的项目里把它当作路线图,遇到瓶颈时回头看看,就知道该在哪一层做调整。
从SVG图里你能看到,解析阶段其实是整个VCF数据整合的"腰部"——上面连着原始文件,下面连着结构化和知识图谱。选库还是自写,没有绝对的好坏,关键看你的数据量、格式规范程度和下游要求。我自己的经验是:先拿cyvcf2快速出原型,遇到定制需求再局部替换成自定义解析,两者互补着用最舒服。