基本语法:Snakefile、规则定义、输入输出声明
一句话认识 Snakefile
说白了,Snakefile 就是一份带有「魔法」的 Python 脚本。
它本身就是一个纯文本文件,命名必须叫 Snakefile(首字母大写)。你可以在里面写标准 Python 代码——import 模块、定义函数、用循环生成规则都没问题。Snakemake 在解析时会自动把它当作 Python 脚本执行一遍,然后从中提取出你需要运行的规则。
我个人习惯把 Snakefile 放在项目的根目录下,这样后续配置、数据和结果都围绕它组织,结构很清楚。
# Snakefile 示例:开头的部分就是纯 Python
import os
SAMPLE_DIR = "data/samples"
RESULTS_DIR = "results"
def get_samples():
return os.listdir(SAMPLE_DIR)
你想想看,有了 Python 加持,你的工作流能灵活到什么程度?动态生成规则、读取 CSV 配置文件、根据环境变量切换参数……统统不在话下。
规则:Snakemake 的「积木块」
规则是整个框架的核心。我最早用 Snakemake 时,总觉得写规则就是在「翻译」自己脑子里的分析步骤。后来我才意识到,理解规则的逻辑比写出规则更重要。
一条规则,本质上是在声明三件事:
- 输入(Input)——你手头有什么数据?
- 输出(Output)——你想得到什么结果?
- 动作(Run / Shell)——中间需要执行什么命令或脚本?
看个最简单的例子:
rule say_hello:
output:
"greeting.txt"
shell:
"echo 'Hello, Snakemake!' > greeting.txt"
这里 rule say_hello 后面的名字是你自己取的,我建议用 项目_步骤 这种方式命名。曾经有一次我在一个批量分析项目里,规则名没起好,结果流程跑完我对着 DAG 图愣是没看懂每个节点是干嘛的……嗯,从那以后我就老老实实定了一套命名规范。
动作部分有两种常见写法,我帮你对比一下:
| 关键字 | 适用场景 | 例子 |
|---|---|---|
shell: |
执行一条 shell 命令或脚本 | shell: "bwa mem {input} > {output}" |
run: |
需要写多行 Python 代码 | run: df = pd.read_csv(input[0]) |
shell: "python my_script.py" 来调用,维护起来会轻松很多。我在团队里推行这个做法后,review 代码的效率明显高了一截。
输入与输出:规则的「接口」
输入和输出声明看起来简单,里面其实有不少门道。
先说说输出:Snakemake 要求你必须明确写出输出文件路径。这不仅是为了让你自己看清楚结果在哪,更是为了让 Snakemake 能够检查文件是否已经生成,避免重复计算。
再来看输入:你可以写一个文件,也可以用列表写多个文件:
rule multi_input_demo:
input:
"data/sample1.fq",
"data/sample2.fq",
"data/sample3.fq"
output:
"results/merged.fq"
shell:
"cat {input} > {output}"
通配符是个好东西,它让一条规则能匹配成千上万个文件。比如:
rule align:
input:
"data/{sample}.fq",
"refs/genome.fa"
output:
"results/{sample}.bam"
shell:
"bwa mem {input[1]} {input[0]} > {output}"
这里的 {sample} 就是通配符。当 Snakemake 发现需要 sample_A.bam 时,它会自动推断出 sample 为 sample_A,然后去寻找 sample_A.fq。嗯,这里要注意,通配符的名字必须在 output 中出现,Snakemake 才能完成这个推理过程。
有一次我遇到一个 bug:某个规则在命令行里单独跑没问题,一放进流程就报错「Missing input files」。查了半天才发现,原来是输入文件名里的通配符拼写跟输出里不一致。你瞧瞧,一个字母的疏忽就能让整个流程卡住。
一图胜千言:规则的内部结构
我画了个示意图,把上面这些概念串在一起,你可以边看边对照着理解。
关键逻辑: 从左到右看「规则名」→「输入/输出」→「动作」,这个过程其实就是在描述一个转换(Transformation)。输出文件是你最终想要的,输入文件是原材料,动作就是加工方法。通配符把规则从「单次操作」变成了「批量工厂」。
实战:一个完整的小流程
说了这么多,我们来写一个真正的 Snakefile 片段。假设我要对一个队列里的样本进行质控,然后比对:
SAMPLES = ["sample_A", "sample_B", "sample_C"]
rule all:
input:
expand("results/{sample}.bam", sample=SAMPLES)
rule fastqc:
input:
"data/{sample}.fq"
output:
"reports/{sample}_fastqc.html"
shell:
"fastqc {input} -o reports/"
rule align:
input:
fq="data/{sample}.fq",
ref="refs/genome.fa"
output:
"results/{sample}.bam"
shell:
"bwa mem {input.ref} {input.fq} | samtools sort > {output}"
这个例子值得你多看两眼:
rule all是 Snakemake 的默认目标规则,它没有任何实际操作,只是声明「我最终需要这些文件」。expand函数根据SAMPLES列表和通配符模板results/{sample}.bam,自动生成三个文件名:results/sample_A.bam、results/sample_B.bam、results/sample_C.bam。- Snakemake 会反向推理:为了得到
sample_A.bam,需要运行align规则;而align需要sample_A.fq,这又会触发fastqc规则……依赖关系自动构建完毕。
避坑指南: 我曾经在 rule all 里漏写了一个样本,结果跑了三天我才发现某个关键结果根本没生成。从那时起,我每次写完 Snakefile 都会先用 snakemake -n 做一次 dry-run,检查一下要运行的全部任务是不是我想要的。你也养成这个习惯,能省很多时间。
到这里,你已经掌握了 Snakemake 最基础的语法框架。说白了就是三样东西:Snakefile 文件结构、规则定义、输入输出声明。后面的章节里,我们会在这个基础上一步步增加复杂度,让流程真正变得健壮和可复用。