第二章 必填列与可选列的陷阱归纳总结

GFF3 文件一共 9 列,每一列都有它自己的脾气。我刚开始处理 GFF3 时,以为只要把数据按顺序塞进去就行,结果被各种隐式规则坑得够呛。今天咱们就逐列过一遍,把那些藏在细节里的坑都挖出来。

GFF3 九列常见陷阱分布图 seqid source type start end score strand phase attributes 空格/特殊字符 大量缺失用"?" SO术语非受控 1-based vs 0-based start > end 分数格式 +/-/. 之外 0/1/2 误写 未转义 ID重复 每列都有其专属陷阱,但常见错误集中在 attributes 和坐标列上

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 ≤ end0-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 的坑最多,坐标列排第二。列序和缺失值占位符是基础,但出错的频率一点都不低。你想想看,一个文件里如果有几十万个特征,光是因为少了一个引号导致的解析报错,就能让整个流程断掉。