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 的磁盘空间。
知识结构总览
为了让你更直观地理解这三块内容的关系,我画了一张图:
我特意把这张图画成三列并列的结构。你会发现:左边 Pipe 管“步骤间数据流动”,中间规则管“每一步的执行参数”,右边输入输出管“文件批量展开”。
一个完整流程的运行逻辑是这样的:Snakemake 先根据输出文件路径里的通配符,反向推导出需要哪些输入文件;然后匹配对应的规则定义,把 resource、threads、log 等信息提取出来;最后通过 Pipe 语法将多个 shell 命令串成一条流水线。
避坑指南 & 实用技巧
讲了这么多理论,我再说几个实战中容易踩的坑:
- 通配符命名冲突:同一个 Snakefile 里,两个规则用了相同名字的通配符,但含义不同。Snakemake 会先匹配第一条规则,导致 B 规则跑不起来。解决办法是命名时加前缀,比如
{sample_id}和{group_id}区分开。 - 资源声明不足:我见过有人把所有规则都写成
threads: 1,结果一个 BWA 比对跑了 3 天。正确的做法是参照工具官方文档,比如 BWA mem 通常建议 8 线程,Trim Galore 建议 2 线程。 - 临时文件过多:滥用
temp()会拖慢流程。因为 Snakemake 会在每个临时文件创建后、下一个依赖规则执行前,反复检查文件是否存在。我的建议是:只在那些占用磁盘极大(比如 >100GB)且下游消费后即可丢弃的文件上用temp()。 - 全局规则混淆:有些新手会把
rule all当成普通规则来写 input。但rule all的 input 只是告诉 Snakemake “最终要产出哪些文件”,它本身不执行任何 shell 命令。不要给它写 output 或 shell。
一个小技巧: 如果你不确定自己的通配符解析是否正确,可以在 Snakefile 开头加一行 print(rules),Snakemake 会把解析后的规则列表全部打印出来。比靠肉眼翻代码快多了。
嗯,到这里 Snakemake 的基础三大件就讲完了。Pipe 语法让流程紧凑,规则定义让调度精细,输入输出模式让批量处理自动化。这三样掌握牢靠了,后面再讲 Nextflow 时,你就会发现很多设计思路是相通的。