Snakemake 基础:Pipe 语法、规则定义、输入输出模式

说实话,很多刚接触 Snakemake 的朋友,第一反应都是:“这不就是个加强版 Makefile 吗?” 嗯,外形确实像,内核却完全不同。我自己早期从 Makefile 迁移过来时,也踩过不少坑。今天咱就把 Snakemake 的三大核心——Pipe 语法、规则定义、输入输出模式——掰开揉碎了讲清楚。

1. Pipe 语法:Snakemake 的“胶水”

Snakemake 的 Pipe 语法,说白了就是把多个 shell 命令串起来,让前一个的输出直接流进后一个的输入。跟 Linux 的 | 类似,但更灵活。

来看一个最典型的例子:

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

上面这种是单步执行。但实际项目中,我们经常需要“把原始数据预处理完再送去做质控”。这时候 Pipe 就派上用场了。

我的习惯是: 能用 Pipe 的绝不用中间文件。一方面减少磁盘 I/O,另一方面也让流程更清爽。

Snakemake 支持用 pipe 关键字显式声明管道:

rule trim_and_qc:
    input:
        "data/sample.fastq"
    output:
        pipe("results/trimmed.fastq"),
        "results/fastqc/sample_fastqc.html"
    shell:
        "cutadapt -a AGATCGGAAGAGC {input} | fastqc stdin:{output[0]} -o results/fastqc/"

注意这里的 pipe()。它的作用是把输出文件标记为“管道中间产物”,Snakemake 不会真正把它写到磁盘,而是直接传给下一个命令。这样可以省掉至少 30% 的 I/O 开销。

这里有个坑: 我曾经在某个项目中用 Pipe 链接了 5 个工具,结果中间某个工具崩溃了,整个链都得重跑。调试起来非常痛苦。所以我的建议是——链条长度控制在 3 个以内,超过就用临时文件兜底。

2. 规则定义:Snakemake 的执行单元

每个 rule 就是 Snakemake 里的一个“执行单元”。你写多少个规则,它就拆成多少个任务。规则的基本结构是:

rule <规则名称>:
    input:
        "路径/文件名"
    output:
        "路径/文件名"
    params:
        # 额外参数,供 shell 或脚本使用
    log:
        "路径/日志文件"
    benchmark:
        "路径/性能记录"
    threads: 8
    resources:
        mem_mb = 16000
    shell:
        "命令 {input} -o {output}"

你可能会问:“为什么 params、log、benchmark、threads、resources 都要单独写出来?塞到 shell 里不行吗?” 我刚开始也觉得麻烦,直到有一次要同时跑 200 个样本的比对任务……

把这些字段拆开,Snakemake 就能精准调度资源。比如 threads: 8 告诉调度器“这个任务要用 8 个核”,resources 则限定内存上限。在集群环境下,这些信息会被传递给 SLURM、SGE 等调度系统。

来一个完整例子:

rule bwa_align:
    input:
        ref = "data/genome.fa",
        fq1 = "data/sample_R1.fastq",
        fq2 = "data/sample_R2.fastq"
    output:
        "results/bam/{sample}.bam"
    params:
        rg = "@RG\\tID:{sample}\\tSM:{sample}"
    log:
        "logs/bwa/{sample}.log"
    threads: 8
    resources:
        mem_mb = 32000
    shell:
        """
        bwa mem -t {threads} -R '{params.rg}' \
            {input.ref} {input.fq1} {input.fq2} \
            2> {log} | samtools sort -o {output} -
        """

看到了吗?threads 和 resources 在这里不是摆设。Snakemake 会根据你定义的资源消耗,自动决定同时跑几个任务。

3. 输入输出模式:通配符与全局规则

Snakemake 最强大的地方,就是它的“通配符模式”(wildcards)。比如你想处理 100 个样本,难道要写 100 个规则?当然不用。只要写一个带通配符的规则,Snakemake 会自动展开。

通配符的基本用法:

rule convert:
    input:
        "data/{sample}.txt"
    output:
        "results/{sample}.csv"
    shell:
        "python scripts/convert.py {input} -o {output}"

这里的 {sample} 就是通配符。当 Snakemake 发现你要生成 results/sampleA.csv 时,它会自动把 {sample} 替换成 sampleA,然后去找 data/sampleA.txt 作为输入。

通配符可以叠加使用:

rule aggregate:
    input:
        "results/{group}/{sample}.csv"
    output:
        "results/{group}/summary.csv"
    shell:
        "python scripts/agg.py {input} > {output}"

不过要注意,通配符越多,Snakemake 解析规则的复杂度就越高。我个人建议:样本级用单通配符,组级或条件级最多用两个通配符。超过 3 个,你的 Snakefile 就快变成“通配符地狱”了。

生产环境原则: 通配符必须有明确的范围约束。比如用 expand() 函数显式列出所有可能的样本名,而不是让 Snakemake 全局搜索。这样既提升性能,又避免意外匹配到垃圾文件。

除了通配符,Snakemake 还有两种特殊的输入输出模式:

  • 别名模式:用 alias() 给文件起别名,方便在规则内引用
  • 模拟输入:用 temp() 标记临时文件,执行完后自动删除

举个例子:

rule clean_sample:
    input:
        "raw/{sample}.fastq.gz"
    output:
        temp("clean/{sample}.fastq.gz")
    shell:
        "zcat {input} | python scripts/filter.py | gzip > {output}"

temp() 告诉 Snakemake:“这个文件用完就丢,不用留着占地方”。在大规模流程中,合理使用临时文件能省出好几个 T 的磁盘空间。

知识结构总览

为了让你更直观地理解这三块内容的关系,我画了一张图:

Snakemake 基础:三大核心支柱 Pipe 语法 显式管道声明 隐式 Shell 串联 管道中间产物 ▸ pipe() 标记中间文件 ▸ 链条控制在 3 步内 ▸ 调式时可用临时文件兜底 规则定义 字段拆解(input/output/params) 资源声明(threads/resources) 日志与基准记录 ▸ 显式声明资源上限 ▸ log/benchmark 单独拆出 ▸ 适用于集群调度 输入输出模式 通配符模式 别名与模拟输入 expand() 显式列举 ▸ 使用通配符自动展开任务 ▸ temp() 标记临时文件 ▸ 通配符 <= 2 个最佳 三者协同关系 Pipe 语法串联步骤 → 规则定义拆分任务 → 输入输出模式实现样本批量处理 三者缺一不可,共同构成可扩展、可复用的生产级流程

我特意把这张图画成三列并列的结构。你会发现:左边 Pipe 管“步骤间数据流动”,中间规则管“每一步的执行参数”,右边输入输出管“文件批量展开”。

一个完整流程的运行逻辑是这样的:Snakemake 先根据输出文件路径里的通配符,反向推导出需要哪些输入文件;然后匹配对应的规则定义,把 resource、threads、log 等信息提取出来;最后通过 Pipe 语法将多个 shell 命令串成一条流水线。

避坑指南 & 实用技巧

讲了这么多理论,我再说几个实战中容易踩的坑:

  1. 通配符命名冲突:同一个 Snakefile 里,两个规则用了相同名字的通配符,但含义不同。Snakemake 会先匹配第一条规则,导致 B 规则跑不起来。解决办法是命名时加前缀,比如 {sample_id} 和 {group_id} 区分开。
  2. 资源声明不足:我见过有人把所有规则都写成 threads: 1,结果一个 BWA 比对跑了 3 天。正确的做法是参照工具官方文档,比如 BWA mem 通常建议 8 线程,Trim Galore 建议 2 线程。
  3. 临时文件过多:滥用 temp() 会拖慢流程。因为 Snakemake 会在每个临时文件创建后、下一个依赖规则执行前,反复检查文件是否存在。我的建议是:只在那些占用磁盘极大(比如 >100GB)且下游消费后即可丢弃的文件上用 temp()。
  4. 全局规则混淆:有些新手会把 rule all 当成普通规则来写 input。但 rule all 的 input 只是告诉 Snakemake “最终要产出哪些文件”,它本身不执行任何 shell 命令。不要给它写 output 或 shell。

一个小技巧: 如果你不确定自己的通配符解析是否正确,可以在 Snakefile 开头加一行 print(rules),Snakemake 会把解析后的规则列表全部打印出来。比靠肉眼翻代码快多了。

嗯,到这里 Snakemake 的基础三大件就讲完了。Pipe 语法让流程紧凑,规则定义让调度精细,输入输出模式让批量处理自动化。这三样掌握牢靠了,后面再讲 Nextflow 时,你就会发现很多设计思路是相通的。