第一类错误:列数不足或过多——列数校验

GFF/GTF 文件是严格按列分隔的表格。我见过最多的报销场景,就是列数不对。多一列、少一列,程序直接崩掉。说白了,这就是个格式规矩问题——你少写一行没事,少写一列,整条记录就认不出来了。

列数标准是什么?

GFF3 规范规定每行必须恰好 9 列,用 TAB 分隔。GTF 同样 9 列,但第 9 列(属性列)的格式不一样。列编号从 1 到 9,顺序是:

  1. seqid——染色体/ scaffold 名字
  2. source——注释来源(比如 GenBank、我们自己跑的 Augustus)
  3. type——特征类型(exon、CDS、gene 这些)
  4. start——起始坐标(1-based)
  5. end——结束坐标
  6. score——得分或置信度(可以填 .)
  7. strand——链方向(+ 或 - 或 .)
  8. phase——阅读框偏移(CDS 必填,其他用 .)
  9. 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:列数校验的流程

下面这张图帮你梳理整个校验流程,从拿到文件到最后修复,逻辑很清晰:

GFF/GTF 列数校验流程 输入 GFF/GTF 文件 awk 统计 NF,找出异常行 NF==9 ? 全部 9 列,通过 列数异常,输出行号 csvkit 结构化复核 定位具体坏行 继续后续校验 发现异常行后,用 awk 提取具体内容,人工或脚本修复 (如去掉多余 TAB、补齐缺失属性)

小技巧:如果你在管道里用 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: ... 目标,每次跑数据前先检验。