第二章 必填列与可选列的陷阱归纳总结
GFF3 文件一共 9 列,每一列都有它自己的脾气。我刚开始处理 GFF3 时,以为只要把数据按顺序塞进去就行,结果被各种隐式规则坑得够呛。今天咱们就逐列过一遍,把那些藏在细节里的坑都挖出来。
2.1 列序:一步错,步步错
GFF3 严格规定列序:seqid、source、type、start、end、score、strand、phase、attributes。顺序乱掉,解析器直接报错,或者更糟糕——静默解析到错误字段。
我记得有一次接手一个同事的 GFF3 文件,看着格式挺规整,可一跑验证工具就报「phase 列出现了字母」。我一看,原来他把 strand 和 phase 的顺序写反了,phase 列里放的却是 + 号。工具不会告诉你「顺序错了」,它只会抱怨数据不合法。
常见的列序问题:
- 把 attributes 放到了 phase 前面——phase 变成一堆键值对字符串
- 漏掉某一列(比如 source 省略了),后方的列集体前移
- 多余的空列——比如以制表符结尾产生第10列
避坑指南:写脚本导出 GFF3 时,务必明确写出每一列,不要依赖列表自动拼接。我曾经用 Pandas 导出 DataFrame,默认把 index 写成了第一列,结果 seqid 整体偏移——你想想看,那排查起来有多痛苦。
2.2 缺失值与占位符「.」的误用
GFF3 规定缺失值用英文句点 . 表示。但很多人会:
- 用空字符串代替(
spacer\t\tgene多了一个制表符) - 用
NA或NULL字符串 - 甚至用问号
?(早期版本规定可以用?表示未知,但当前官方标准已废弃)
警告:对于 score、strand、phase 等可选列,如果不提供值,必须写 . 占位。写 - 或直接省略会导致列数不足。我记得有个数据库导入时,因为 score 列用了 NA,整个文件被当作非法记录丢弃,丢了上千条注释。
我个人的习惯是:生成 GFF3 时,所有缺失值统一用 .,并且在最后一步做列数校验:每行必须有恰好 9 个字段,用制表符分隔。写一个简单的 shell 命令就能检查:
awk -F'\t' 'NF!=9 {print "行 " NR " 有 " NF " 列"}' your_file.gff3
2.3 第9列 attributes:陷阱重灾区
attributes 是 GFF3 最灵活也最易出错的一列。它要求用分号分隔的键值对,值必须用双引号引起来(如果值包含空格或特殊字符)。我见过的典型错误:
- 引号不配对——
ID=gene1;Name=my gene缺少引号,解析器会把my和gene当作两个 token - 分号未转义——如果值本身包含分号,须用
%3B转义 - ID 重复——同一个文件中,不同特征的 ID 不能相同
- Parent 引用不存在——子特征引用的 Parent ID 必须在文件中已定义
提示:我建议所有 attributes 里的值都用双引号包裹,即使不必须。这样能避免后续添加空格时忘加引号。像是这样:
ID=gene001;Name="my favorite gene";note="see comment"
还有一个隐蔽的坑:分号后可以有空格吗?标准说可接受,但不是所有解析器都兼容。最好统一无空格——key=val;key2=val2。我曾经在一个大型项目中,一半工具接受空格一半不接受,最后不得不写个全局替换脚本把分号后的空格全部去掉。
2.4 坐标列:1-based 与整数边界
GFF3 坐标是 1-based、闭区间。也就是说,第 100 个碱基的起始位置是 100,终点是 100。这和 BED 格式(0-based)完全不同。很多从 BED 转 GFF3 的人会忘记 +1 操作,结果坐标偏了一位。
另外,start 必须 ≤ end。但有的工具会生成反链特征(比如负链上的基因),依然要保证 start 是较小的数值,end 是较大的数值。方向由 strand 列控制,不是靠坐标颠倒。
避坑指南:我曾经处理过一个来自第三方的 GFF3,它把负链基因的 start 写成了较大的数,end 写成了较小的数。解析器没有报错,但下游分析全部反转——找 exon 的时候坐标范围完全错了。所以一定记住:坐标永远 start ≤ end。
2.5 其他列的常见误写
| 列名 | 允许值 | 常见错误 |
|---|---|---|
| seqid (第1列) | 字母、数字、下划线、点、连字符 | 含空格、/ 、:等,序列ID与FASTA头部不匹配 |
| source (第2列) | 任意文本,常用 "." 占位 | 缺少引号,把程序名省略为空格 |
| type (第3列) | SO 术语(推荐)或受控词 | 用 free text 如 "exon_region"(应为 "exon") |
| start/end (4-5列) | 正整数,start ≤ end | 0-based坐标,或start > end |
| score (第6列) | 浮点数或 "." | 用整数且无小数点,或填写科学计数法(部分旧工具不认) |
| strand (第7列) | +、-、. | 写成 forward/reverse,或使用字母(如 F、R) |
| phase (第8列) | 0、1、2 或 . | CDS特征填写其他数字,或忽略CDS的phase |
2.6 实战:一个典型的坏文件
咱们看一个真实反面教材:
chr1 . gene 100 200 . + . ID=gene1;Name=my gene;parent=
chr1 . exon 100 150 . + . ID=exon1;Parent=gene1
chr1 . CDS 100 150 . + 1 ID=cds1;Parent=gene1;note="CDS;important"
你能找出几个问题?
- 第1行:
Name=my gene缺少引号,应该Name="my gene" - 第1行:
parent=后面是空值,应该直接省略或保持为.,但作为键值对必须有值,实际应删除该键值对 - 第3行:
note="CDS;important"分号未转义,应该note="CDS%3Bimportant" - 第1行和第3行都缺少 features 之间的 ID 唯一性检查(这里没问题,但易忽略)
我的建议:写 GFF3 之后一定跑一遍 gt gff3validator 或者 gff3-validator(Python包)。不要相信肉眼审查——我自己就吃过肉眼漏看的亏,一个少写引号的文件居然跑完了整个pipeline,最后结果图全歪了。
总结一下:9 列中,attributes 的坑最多,坐标列排第二。列序和缺失值占位符是基础,但出错的频率一点都不低。你想想看,一个文件里如果有几十万个特征,光是因为少了一个引号导致的解析报错,就能让整个流程断掉。