Snakefile 基础:结构、规则、文件与通配符
Snakefile,说白了就是 Snakemake 的“剧本”。你告诉它从哪读数据、怎么处理、输出什么,它就把整个流程串起来。我刚开始接触时总觉得这东西不过是个配置文件,后来项目一复杂才发现——没有清晰的 Snakefile,你根本没法管理上百个样本的处理逻辑。
Snakefile 的整体结构
一个典型的 Snakefile 由三大部分组成:配置区、辅助函数或全局变量、以及一条条规则。规则是核心,配置是调料,辅助函数是工具。我习惯把所有规则放在文件后段,前面只放全局设置和导入。
# 配置区
configfile: "config.yaml"
# 全局变量
SAMPLES = ["A", "B", "C"]
# 规则定义
rule all:
input:
expand("results/{sample}/report.html", sample=SAMPLES)
rule process:
input:
"data/{sample}.fastq"
output:
"results/{sample}/cleaned.fastq"
shell:
"fastp -i {input} -o {output}"
rule report:
input:
"results/{sample}/cleaned.fastq"
output:
"results/{sample}/report.html"
script:
"scripts/generate_report.py"
你看,每个规则都像一个小函数:定义输入、输出,然后告诉 Snakemake 怎么执行。这就好比你写了一个菜谱,每一步都有明确的原材料和成品。
规则(rule)的定义
规则是 Snakefile 的最小执行单元。一个规则包含三个基本要素:input(输入)、output(输出)、执行指令(shell、script、run 或 notebook)。举个例子:
rule trim_reads:
input:
"raw/{sample}_R1.fastq.gz",
"raw/{sample}_R2.fastq.gz"
output:
"trimmed/{sample}_R1.fastq.gz",
"trimmed/{sample}_R2.fastq.gz"
threads: 4
shell:
"trimmomatic PE {input} {output} ..."
这里我设定了线程数和双端输入输出。嗯,这里要注意:threads 和 resources 是可选但很实用的字段,尤其是你跑在集群上时,不设线程数可能调度得特别慢。
规则执行方式的三种选择
shell:直接写命令行,适合成熟工具(如 fastp、samtools)。script:调用外部 Python 或 R 脚本,适合复杂逻辑。run:内嵌 Python 代码,适合短逻辑或参数组装。
我个人偏爱 shell,因为调试时可以直接复制命令到终端跑。
输入输出文件
输入输出是规则的“门牌号”。Snakemake 根据输出文件的名字自动推导依赖关系。你想想看,如果你手写 Makefile,得手动维护依赖树;而 Snakemake 通过文件名里的通配符自动匹配。比如:
rule all:
input:
"results/final_summary.txt"
rule analyze_chr:
input:
"data/chr{chrom}.vcf"
output:
"results/chr{chrom}_stats.txt"
shell:
"bcftools stats {input} > {output}"
analyze_chr 规则里的 {chrom} 就是通配符。Snakemake 看到 all 规则需要 final_summary.txt,它不会直接跑 analyze_chr,而是先看有没有别的规则能生成那个文件。没有的话……报错。所以我总会加一条 rule all 作为总入口,避免迷路。
| 字段 | 作用 | 常见陷阱 |
|---|---|---|
input | 声明规则依赖的文件 | 路径写错或通配符不匹配会导致循环依赖 |
output | 声明规则生成的文件 | 输出文件必须与输入文件共享同一个通配符,否则无法合并 |
wildcard_constraints | 限制通配符匹配模式 | 不设约束可能匹配到不该匹配的目录 |
我曾经在一次项目中忘记在 output 里写通配符,结果 Snakemake 以为每个样本都输出同一个文件名,直接覆盖掉前面的结果。从那以后我每写一条规则都检查一遍通配符的统一性。
通配符的基本使用
通配符是 Snakefile 的魔法。它允许你用一条规则处理所有样本。最基本的写法是 {name},Snakemake 会自动从目标文件名中提取。比如你要处理样本 X、Y、Z,只要在 rule all 里用 expand() 展开:
rule all:
input:
expand("results/{sample}/done.txt", sample=["X", "Y", "Z"])
rule process_sample:
input:
"data/{sample}.csv"
output:
"results/{sample}/done.txt"
shell:
"echo processed > {output}"
这里 expand() 会生成三个路径:results/X/done.txt、results/Y/done.txt、results/Z/done.txt。Snakemake 看到 process_sample 的输出可以匹配这些路径,就会自动为每个样本跑一次。你还可以用 wildcard_constraints 限制匹配格式,比如只匹配数字:
rule filter_variants:
input:
"data/chr{chrom}.vcf"
output:
"results/chr{chrom}_filtered.vcf"
wildcard_constraints:
chrom = "chr[0-9]+"
shell:
"gatk FilterVariants -V {input} -O {output}"
为什么要加约束?我在一个批处理项目中遇到过:数据目录里有 data/chrM.vcf(线粒体),而我本来只想处理常染色体。没加约束,结果把线粒体也跑了一遍,浪费半天时间。
如果你想传递多个通配符,比如 results/{sample}_{condition}.tsv,只要保证输入和输出使用相同的通配符名称即可。Snakemake 会自动匹配对应位置。
通配符还能结合 expand() 和 zip 实现多维度组合。比如你有两个样本列表和两种处理方式,想生成所有组合:
SAMPLES = ["A", "B"]
CONDITIONS = ["t1", "t2"]
rule all:
input:
expand("results/{sample}_{condition}.txt",
zip, sample=SAMPLES, condition=CONDITIONS)
这样只会生成 A_t1.txt 和 B_t2.txt,而不是笛卡尔积。这种用法在处理配对样本时特别顺手。
这张图展示了我写 Snakefile 时最核心的流程:rule all 作为入口,通过 expand() 展开所有目标文件 → Snakemake 根据输出文件名反向匹配到 process_sample 规则 → 检测输入文件是否存在 → 执行 shell 指令 → 生成实际结果。通配符就是连接这些步骤的“胶水”。
总结几个实用技巧:
- 始终保留一个
rule all作为最终目标,否则 Snakemake 默认跑第一条规则。 - 通配符名称在输入和输出中必须一致,大小写敏感。
- 用
wildcard_constraints避免匹配到意外路径。 - 出错时加
--dry-run看看规则计划,能省大量调试时间。