4. Snakefile 文件结构:全局参数、规则定义、包含文件

Snakefile 是 Snakemake 的核心。说白了,它就是一个 Python 文件,只不过加了一些特殊的语法糖。但你千万不要把它当成普通 Python 脚本来写。一个好的 Snakefile 结构,能让你的流程清晰、可维护,团队协作起来也顺畅。

我个人习惯把 Snakefile 拆成三大块:全局参数、规则定义、包含文件。这三块各司其职,组合起来就是一套完整的工作流。为什么要这么区分呢?我来一个个讲清楚。

Snakefile 结构三大块关系图 Snakefile 结构三大核心块 全局参数 config、路径、samples 规则定义 rule、input、output、run 包含文件 include、模块化、复用 说明 左→中→右:全局参数 → 规则定义 → 包含文件 包含文件可以引用规则定义,全局参数被所有规则共享 实际项目建议:配置放 config.yaml,核心规则放 Snakefile,辅助规则用 include

4.1 全局参数:流程的“总开关”

全局参数放在 Snakefile 的最顶部。我建议把 configfile、samples、output 等变量集中在这里。这样做的好处是,你一眼就能看到整个流程的输入输出配置,不用翻遍几百行代码去找一个路径变量。

举个例子,这是我在一个 RNA-seq 项目里用的全局参数区域:

configfile: "config.yaml"

# 读取样本信息
samples = pd.read_table(config["sample_table"])
sample_names = list(samples["sample_id"])

# 定义输出根目录
OUTDIR = config["output_dir"]
REPORTS = f"{OUTDIR}/reports"
LOGS = f"{OUTDIR}/logs"

# 定义软件路径(方便切换版本)
FASTQC = config["fastqc_path"]
TRIM = config["trimmomatic_path"]

你看,我把 configfile 放在第一行,然后读取样本表,最后定义几个全局路径变量。为什么这么搞?因为如果后期要改样本或路径,我只需要改 config.yaml,Snakefile 本身不动就好了。

我的小技巧: 全局变量名用全大写,比如 OUTDIR、REPORTS,能让你在规则里一眼分出哪些是来自全局的固定值,哪些是局部变量。我在项目里一直这么用,团队 review 时也轻松不少。

4.2 规则定义:流程的“原子任务”

规则是 Snakefile 里最核心的部分。每条规则就是一个独立的任务,比如质控、比对、定量。我刚开始写 Snakefile 时,总想把所有事情塞进一条规则里——嗯,结果就是又慢又难调试。

后来我学乖了:一条规则只做一件事。比如这样:

rule fastqc:
    input:
        "{sample}_R1.fastq.gz"
    output:
        "fastqc/{sample}_fastqc.html"
    log:
        "logs/fastqc/{sample}.log"
    params:
        threads = 4
    run:
        shell("fastqc -t {params.threads} {input} -o fastqc/ > {log} 2>&1")

rule trimmomatic:
    input:
        r1 = "{sample}_R1.fastq.gz",
        r2 = "{sample}_R2.fastq.gz"
    output:
        r1_trim = "trimmed/{sample}_R1_trim.fastq.gz",
        r2_trim = "trimmed/{sample}_R2_trim.fastq.gz"
    log:
        "logs/trimmomatic/{sample}.log"
    params:
        adapters = "adapters/TruSeq3-PE.fa"
    run:
        shell("trimmomatic PE -threads 4 {input.r1} {input.r2} "
              "{output.r1_trim} {output.r2_trim} "
              "ILLUMINACLIP:{params.adapters}:2:30:10 > {log} 2>&1")

这里我有几点想提醒你:

  • input 和 output 要用通配符 {sample},这样一条规则能处理所有样本。
  • log 文件我建议都写进去,别偷懒。有一次我在项目里没写 log,跑了一夜结果报错,根本不知道哪一步出的问题,气得我肝疼。
  • params 可以用来传递参数,比如线程数、参考基因组路径。这样规则内部运行 shell() 时更清晰。
注意: 规则里的 output 文件一定要被其他规则或最终目标规则用到,否则这条规则永远不会被触发。我曾经犯过这个错,写了条规则输出一个临时文件,结果主流程根本不理它。

4.3 包含文件:给 Snakefile 瘦身

项目一大了,Snakefile 会变得臃肿。我见过有人把两百条规则堆在一个文件里,打开滚动条都拖半天。这种时候就要用 include 来分割了。

包含文件的思想很简单:把功能相近的规则抽到单独的文件里,然后在主 Snakefile 里用 include 引入。比如:

# 主 Snakefile
configfile: "config.yaml"
include: "rules/fastqc.smk"
include: "rules/trimming.smk"
include: "rules/alignment.smk"
include: "rules/quantification.smk"

# 最终的汇总规则
rule all:
    input:
        expand("reports/{sample}.qc_complete", sample=sample_names)

你看,这样主 Snakefile 就只剩几行 include 和一条 rule all 了。每个 .smk 文件里只放对应模块的规则,结构和调试都方便多了。

包含文件的写法跟 Snakefile 一样,只是文件后缀用 .smk 以示区别。比如 rules/fastqc.smk:

# rules/fastqc.smk
rule fastqc:
    input:
        "{sample}_R1.fastq.gz"
    output:
        "fastqc/{sample}_fastqc.html"
    shell:
        "fastqc -t 4 {input} -o fastqc/"
项目经验: 我曾经在一个大型单细胞项目里,把所有预处理规则放在一个 smk 文件,比对放在另一个,定量放在第三个,最后用一个 master Snakefile 组织起来。团队里每个人只需要维护自己负责的模块,互不干扰。这个经验我建议你在团队协作时试试。

4.4 推荐目录与文件结构

说了这么多,给你一个我常用的项目结构作为参考:

文件/目录 作用
Snakefile 主控文件,包含全局参数和 include 语句
config.yaml 配置文件,存放样本表路径、软件路径、参数
rules/ 存放所有 .smk 规则文件
data/ 原始数据,按样本或批次存放
results/ 分析结果输出目录
logs/ 运行日志,对应每条规则的 log 路径

你可以直接把这个结构用到项目中,减少自己设计的时间。记得把 config.yaml 写得详细点,比如:

# config.yaml
sample_table: "data/samples.csv"
output_dir: "results"
fastqc_path: "/usr/bin/fastqc"
trimmomatic_path: "/usr/bin/trimmomatic"
star_path: "/opt/STAR/bin/Linux_x86_64/STAR"
threads: 8
memory: 32
避坑指南: 路径尽量不要用硬编码。我在一个项目里直接写了 /home/user/data/,结果换服务器跑全炸了。现在我只用 config.yaml 里的相对路径,移植到哪儿都行。

4.5 一点收尾思考

全局参数、规则定义、包含文件这三块,看似简单,但用好了能让你的 Snakemake 流程从“能跑”变成“优雅地跑”。我个人最享受的就是把所有逻辑拆到单独的 smk 文件里,看着主 Snakefile 干干净净的,心里就舒服。你试试看,绝对上瘾。