Snakefile 的骨架:规则定义与执行

好,从这章开始我们正式动手写 Snakefile。说实话,我第一次看 Snakefile 时觉得它像个奇怪的混血儿——长得像 Python,又有点 Makefile 的影子。但用顺手后你会发觉,这玩意就是为生信流程量身定做的。

4.1 规则(rule)—— 工作流的原子单位

Snakefile 里一切从规则开始。每条规则定义了三个核心要素:

  • input:这条规则需要什么文件
  • output:这条规则产生什么文件
  • shell/run/script:如何从 input 得到 output

看一个最简单的例子:

rule fastqc:
    input:
        "data/sample1_R1.fastq.gz"
    output:
        "results/fastqc/sample1_R1_fastqc.html"
    shell:
        "fastqc {input} -o results/fastqc/ 2>/dev/null"

这里面有个细节要注意:{input} 和 {output} 是 Snakemake 的内置变量,运行时会被自动替换成实际路径。我个人习惯在 shell 命令里用花括号引用它们,而不是硬编码文件名——这样规则才能泛化复用。

小贴士:output 的文件名可以跟 input 不同,但最好保持一致。比如 fastqc 的输出名是自动生成的(原文件名加 _fastqc.html),你写 rule 时就得事先知道这个规律。

4.2 输入与输出 —— 不只是路径列表

很多初学者以为 input/output 就是写文件名,其实它们还能做更多。

4.2.1 单文件 vs 多文件

如果规则需要多个输入,用逗号分隔即可:

rule merge_fastq:
    input:
        r1 = "data/sample1_R1.fastq.gz",
        r2 = "data/sample1_R2.fastq.gz"
    output:
        "results/merged/sample1.fastq.gz"
    shell:
        "cat {input.r1} {input.r2} > {output}"

注意这里给输入命名了(r1, r2),后面在 shell 里可以用 {input.r1} 引用。如果你不命名,Snakemake 会用 {input[0]}、{input[1]} 这种索引方式——但我建议能命名就命名,可读性提升不止一个档次。

4.2.2 通配符(wildcards)—— 工作流的灵魂

单条规则只能处理一个样本,那 50 个样本怎么办?难道写 50 条 rule?别慌,Snakemake 用通配符解决这个问题。

rule fastqc:
    input:
        "data/{sample}_R1.fastq.gz"
    output:
        "results/fastqc/{sample}_R1_fastqc.html"
    shell:
        "fastqc {input} -o results/fastqc/"

这里的 {sample} 就是一个通配符。当 Snakemake 发现目标文件 results/fastqc/sample1_R1_fastqc.html 不存在时,它会自动把 {sample} 匹配成 sample1,然后去找 data/sample1_R1.fastq.gz。说白了,通配符就是规则里的“变量”,在运行时被真正的名字替换。

避坑指南:通配符不能重名,也不能随意组合。我曾遇到过两个规则用了相同的通配符但含义不同,结果匹配错了文件。记得保持命名一致,比如都叫 {sample}。

4.3 shell 命令 —— 执行与日志控制

shell 里写的是你在命令行实际敲的东西。但要注意几点:

  • Snakemake 会为每条规则创建独立的临时 shell 子进程
  • 默认情况下,标准输出和错误会打印到终端;可以用 2>/dev/null 或 >& /dev/null 静默
  • 如果 shell 命令很长,可以用反斜杠换行

但更推荐的做法是:把复杂的多步骤命令写成单独的脚本(Python 或 R),然后在 rule 里用 script: 指定。这样代码更容易维护。比如:

rule alignment:
    input:
        r1 = "data/{sample}_R1.fastq.gz",
        r2 = "data/{sample}_R2.fastq.gz"
    output:
        "results/bam/{sample}.sorted.bam"
    script:
        "scripts/align_star.py"

脚本里可以通过 snakemake.input、snakemake.output 获取文件路径。这个我们后面章节再细讲。

4.4 运行方式 —— 让工作流转起来

写好的 Snakefile 怎么跑?核心命令就一条:

snakemake --cores 4

这会执行 Snakefile 中第一个规则(如果没指定目标)。如果你只想跑特定规则,可以加在命令后面:

snakemake results/fastqc/sample1_R1_fastqc.html --cores 4

更常用的调试命令:

  • 干跑(dry run):snakemake -n —— 只显示要执行的步骤,不实际运行。必用!
  • 可视化 DAG:snakemake --dag | dot -Tpng > dag.png —— 画出任务依赖图,一眼看穿流程。我每次写新流程都会先看一眼这个图。
  • 强制重新运行:snakemake -R all —— 忽略已有输出,全部重来。
  • 指定工作目录:snakemake --directory /path/to/project
关键点:Snakemake 通过对比 output 文件的时间戳来判断是否需要重新运行。如果 output 比 input 新,则跳过;否则重新执行。所以当你修改了某个 input 文件,Snakemake 会自动只跑受影响的部分——这就是增量计算的价值。

4.5 规则依赖与默认目标

Snakefile 通常包含多条规则,它们通过输出-输入关系形成依赖链。比如:

rule all:
    input:
        "results/multiqc_report.html"

rule multiqc:
    input:
        expand("results/fastqc/{sample}_R1_fastqc.html", sample=SAMPLES)
    output:
        "results/multiqc_report.html"
    shell:
        "multiqc results/fastqc/ -o results/"

rule fastqc:
    input:
        "data/{sample}_R1.fastq.gz"
    output:
        "results/fastqc/{sample}_R1_fastqc.html"
    shell:
        "fastqc {input} -o results/fastqc/"

这里的 rule all 很特殊——它是整个流程的入口。Snakemake 先看 all 需要什么,然后回溯去找能够生成这些文件的规则。我习惯把 rule all 放在文件最前面,用 input 列出最终结果文件,这样运行时就只需要 snakemake --cores 4,不用再指定目标。

经验之谈:实际项目中,rule all 往往很长,包含几十个样本的最终文件。这时可以用 expand() 函数自动展开,比如 expand("results/{sample}.bam", sample=SAMPLES)。

4.6 整体流程一览

Snakefile 运行核心流程 1. 书写 Snakefile 定义 rule(input/output/shell) 使用通配符 {sample} 设置 rule all 入口 2. 运行 snakemake -c4 解析 DAG,只运行过时任务 并行执行,输出结果文件 说明:Snakemake 根据 rule 的 output 文件是否存在及 时间戳决定是否执行。

4.7 常见问题与解决

现象 原因 解决办法
提示 MissingInputException 某个规则的 input 文件不存在 检查路径和通配符是否匹配;用 snakemake -n 确定哪个规则先跑
提示 MissingOutputException shell 命令没有产生预期的 output 文件 检查 shell 命令中输出路径是否正确,或是否被重定向
规则没执行 output 已存在且比 input 新 用 snakemake -R 规则名 强制重新运行

嗯,这里再分享两个小技巧。第一,写规则时先别跑全流程,先在终端手动敲一遍 shell 命令确认能跑通。第二,善用 snakemake --cores 1 -n -p 打印出即将执行的每条命令,确认通配符展开正确。我曾经因为通配符写错一个字母,排查了两个小时……从那以后我每次都先干跑。

Snakefile 的语法基础就这些。现在你可以试着写一个最简单的规则,从输入输出开始,体会 Snakemake 是怎么“理解”你的意图的。