第一类错误:列数不足或过多——列数校验
GFF/GTF 文件是严格按列分隔的表格。我见过最多的报销场景,就是列数不对。多一列、少一列,程序直接崩掉。说白了,这就是个格式规矩问题——你少写一行没事,少写一列,整条记录就认不出来了。
列数标准是什么?
GFF3 规范规定每行必须恰好 9 列,用 TAB 分隔。GTF 同样 9 列,但第 9 列(属性列)的格式不一样。列编号从 1 到 9,顺序是:
- seqid——染色体/ scaffold 名字
- source——注释来源(比如 GenBank、我们自己跑的 Augustus)
- type——特征类型(exon、CDS、gene 这些)
- start——起始坐标(1-based)
- end——结束坐标
- score——得分或置信度(可以填 .)
- strand——链方向(+ 或 - 或 .)
- phase——阅读框偏移(CDS 必填,其他用 .)
- attributes——属性键值对(GFF3 用分号分隔,GTF 用引号和空格)
我见过不少人把第 9 列当成自由文本,结果里面的引号没配对,直接被工具拒绝。你想想看,工具是按列解析的,多了一个 TAB 就变成第 10 列,少了一个 TAB 就变成第 8 列扔出异常。
为什么会多列或少列?
常见原因就两个:
- 属性列里带了未转义的 TAB——比如某些基因描述里恰巧按了 TAB 键,这会在第 9 列里插入一个分隔符,后面的内容就推到第 10 列去了。
- 行尾有多余的 TAB——尤其是从 Windows 文本编辑器 Copy/Paste 过来的文件,行尾可能会残留一个不可见 TAB。
- 注释行混入数据行——有些软件会在 GFF 里写#开头的注释,但读数据时没跳过,把注释也当作一行来数列,结果列数不够。
我以前处理过一个水稻注释文件,五千多行里只有 3 行多了 TAB,却导致整个文件被 UCSC 的 browser 拒绝加载。排查了一整天……最后用 awk 扫一遍就抓出来了。
快速检查:用 awk 统计每行列数
最直接的办法,遍历每一行,打印行号和列数。下面这个命令会忽略注释行(以 # 开头):
awk '!/^#/{print NR, NF}' your_file.gff3 | head -20
NF 是 awk 内置的字段数(以默认空白分隔)。GFF 是 TAB 分隔,所以 awk 默认就能正确识别。如果你确认文件用了 TAB,可以加上 -F'\t' 确保:
awk -F'\t' '!/^#/{print NR, NF}' your_file.gff3 | head -20
输出示例:
1 9
2 9
3 10
4 9
5 9
...
看到第 3 行是 10 列,说明有问题。接下来把多列的行单独拎出来:
awk -F'\t' '!/^#/ && NF != 9' your_file.gff3
想看看少列的行?改成 NF != 9 一样包括。建议同时检查 NF < 9 和 NF > 9:
echo "=== 少于9列的行 ==="
awk -F'\t' '!/^#/ && NF < 9' your_file.gff3
echo "=== 多于9列的行 ==="
awk -F'\t' '!/^#/ && NF > 9' your_file.gff3
我曾用这个命令在五分钟内定位到一个 2GB 的 GFF 文件里所有的坏行,比任何 GUI 编辑器都快。
使用 csvkit 进行结构化检测
awk 虽然快,但输出不够结构化。如果你喜欢用 Python 生态的工具,csvkit 对 GFF 也很友好(虽然它名字带 csv,但默认用 TAB 分隔)。先安装:
pip install csvkit
然后用 csvstat 看列数统计:
csvstat -d $'\t' your_file.gff3
它会打印每一列的列名、空值个数、唯一值个数、数据类型。但是注意——csvkit 默认会把第一行当作表头。GFF 没有表头,需要加 --no-header-row:
csvstat -d $'\t' --no-header-row your_file.gff3
输出长得像这样:
1. "column1"
Nulls: 0
Unique: 7
...
2. "column2"
Nulls: 0
Unique: 4
...
9. "column9"
Nulls: 0
Unique: 1234
如果某一行多列,csvkit 在读取时就会报错,比如 Expected 9 fields, saw 10。我最喜欢用它做快速数据探查,尤其是在合作方发来的文件里,一眼就能看出问题行号。
实操口诀:先 awk 扫全文件,再用 csvkit 做概要验证。后者更直观,但前者更轻量。
验证列数的最佳实践
光靠人工肉眼翻文件肯定不行。我建议在正式处理前,加一个自动校验步骤,用脚本拦截坏行。下面是一个简单的 bash 函数,你可以在 pipeline 里调用:
check_gff_columns() {
local file="$1"
local expected=9
local bad_lines
bad_lines=$(awk -F'\t' '!/^#/ && NF != '"$expected"'' "$file")
if [ -n "$bad_lines" ]; then
echo "ERROR: 发现列数异常的行"
echo "$bad_lines"
return 1
fi
echo "OK: 所有数据行都是 $expected 列"
return 0
}
把这个函数放在脚本开头,文件校验通过再往下走。我合作的一个基因组中心就靠这步,从上游数据管道里拦截了多次格式污染。
SVG:列数校验的流程
下面这张图帮你梳理整个校验流程,从拿到文件到最后修复,逻辑很清晰:
小技巧:如果你在管道里用 grep -v "^#" 去注释,注意有些 GFF 文件注释行里也包含 TAB,但注释行不算数据,列数校验应该跳过它们。上面 awk 命令里的 !/^#/ 就干这个。
修复多列问题:常见场景
一旦抓到坏行,怎么修?
| 异常类型 | 典型表现 | 修复方法 |
|---|---|---|
| 多列(NF=10) | 最后多了一个 TAB,或者属性中包含 TAB | 用 sed 删除行尾 TAB:sed 's/\t$//';若是属性内 TAB,要把那个 TAB 替换成空格:awk -F'\t' '{$9=gensub(/\t/," ","g",$9); print}' |
| 多列(NF=10+) | 属性列里有多个未转义 TAB | 定位坏行,手动清理或写一个基于字段连接的脚本:用 awk 把第 9 列之后的所有字段合并回去 |
| 少列(NF=8) | 最后一列的属性丢了 | 加上一个空属性 . 或者 ID=missing,要根据上下文补 |
| 少列(NF < 8) | 整行被截断,比如最后几个字段全没了 | 回溯源文件,这条记录可能被破坏,建议直接删除或重新生成 |
警告:千万别用 Excel 直接打开 GFF 文件!Excel 会把长数值转成科学计数法,还会吞掉前导零。我见过一个染色体编号“chr01”被自动改成“chr1”的惨案。要用 awk / csvkit 这类命令行工具处理。
实战:一次完整的检查脚本
这里给出一段完整的 shell 脚本,你可以直接集成到你的生物信息学工作流里:
#!/bin/bash
# file: check_gff_columns.sh
INPUT=$1
EXPECTED=9
echo ">>> 1. 用 awk 扫描所有数据行..."
awk -F'\t' '!/^#/{
if (NF != '"$EXPECTED"') {
print "行 " NR " 有 " NF " 列 (期望 " '"$EXPECTED"' ")"
}
}' "$INPUT" > bad_lines.txt
if [ -s bad_lines.txt ]; then
echo "发现以下问题行:"
cat bad_lines.txt
echo ">>> 2. 用 csvkit 进一步确认..."
csvstat -d $'\t' --no-header-row "$INPUT" 2>&1 | grep -i "expected" || echo "csvkit 未报告错误(可能数据本身还是可读的)"
exit 1
else
echo "所有数据行列数正确 ($EXPECTED 列)。通过!"
fi
我在处理 NCBI 下载的 GFF 文件时经常用这个脚本——它们偶尔会因为版本更新在属性里加入多余的分隔符。
本章小结
列数错误是最容易修复的,但也是隐藏最深的。用 awk 扫一遍,几秒钟就能知道文件健不健康。如果你已经开始编写注释处理管道,一定要把列数校验作为第一步。我个人习惯在 Makefile 里加上 check: ... 目标,每次跑数据前先检验。