Snakefile 基础:规则定义、输入输出、通配符基本用法
说到 Snakemake,最核心的东西就是规则。你写的工作流,归根结底是一堆规则的组合。规则定义“做什么”,输入输出告诉 Snakemake “原料在哪、成品放哪”,通配符则让规则能批量处理文件。这三个东西配合好,你的 pipeline 才真的动起来。
我记得刚接触 Snakemake 时,总把规则写成一个个硬编码的 Python 函数。结果数据一变,就得手动改几十处。后来我学会用通配符加输入输出模式,才发现原来工作流可以这么灵活。好,我们一个个拆开讲。
1. 规则如何定义?—— 最简骨架
一个规则长得像这样:
rule my_first_rule:
input:
"data/input.txt"
output:
"results/output.txt"
shell:
"cp {input} {output}"
看起来简单吧?但它已经包含了三个关键部分:input 告诉 Snakemake 需要哪些文件,output 声明生成哪些文件,shell 则给出执行命令。Snakemake 会先检查 output 是否存在,如果不存在,就找 input 是否可获取,然后执行 shell 里的命令。
这里我踩过一次坑:output 的路径如果写了相对路径,Snakemake 默认以 Snakefile 所在目录为工作目录。如果你不小心把输出写到了别的地方,后面的规则就找不到它。我建议你从一开始就把所有路径都写清楚,或者用 config 变量统一控制。
提示: 一个 Snakefile 可以包含多个规则。Snakemake 会根据最终目标文件(你在命令行指定的那个)反向推导出需要运行哪些规则。这叫 DAG 驱动。
2. input 和 output:不仅仅是文件名
除了简单的字符串,input 和 output 还可以写更复杂的表达式。比如多个输入文件:
rule multi_input:
input:
"data/sample1.fq",
"data/sample2.fq"
output:
"results/merged.bam"
shell:
"cat {input} > {output}"
甚至可以用命名输入:
rule named_input:
input:
reads="data/{sample}.fq",
barcode="metadata/{sample}.barcode"
output:
"results/{sample}.counts"
shell:
"tool --reads {input.reads} --barcode {input.barcode} > {output}"
命名输入的好处是,当你的命令参数很杂乱时,代码可读性会好很多。我个人习惯在有 3 个以上输入文件时用命名方式。
关键点: output 是规则的“承诺”——Snakemake 相信规则执行完后这些文件会存在。如果规则没生成它们,Snakemake 会报错,认为规则失败了。
还有一点:output 里不能用 ... 或者 * 通配符,只能用 {wildcard} 这种花括号通配符。后面我们会细讲。
3. 通配符:让规则“活”起来
通配符是 Snakemake 最强大的特性之一。它让你用一个规则模板处理成百上千个样本。看个例子:
rule bwa_align:
input:
"data/{sample}.fastq"
output:
"results/{sample}.bam"
shell:
"bwa mem ref.fa {input} | samtools sort > {output}"
这里 {sample} 就是一个通配符。当 Snakemake 需要生成 results/NA12878.bam 时,它会自动把 {sample} 替换成 NA12878,然后去找对应的输入文件 data/NA12878.fastq。如果输入存在,就执行命令;如果不存在,它会尝试找其他规则来生成它。
我曾经有一个项目,需要处理 3000 个 WGS 样本。如果每个样本写一条规则,写三天都写不完。但用了通配符,一个规则就搞定了。当然,你一定要保证通配符在 input 和 output 中都能被解析——如果通配符只在 output 中出现而不在 input 中出现,Snakemake 就没法知道输入文件是什么,会报错。
警告: 通配符名称要一致。比如你在 output 里写 {sample}.bam,input 里写 {sample}.fastq,那么通配符名字必须都是 sample,不能一个写 sample 一个写 id。否则 Snakemake 会认为两个是不同的通配符,无法自动匹配。
4. 通配符约束:精准匹配
有时候,你只希望通配符匹配某些格式。比如只允许字母数字和下划线:
rule fastqc:
input:
"data/{sample}.fastq"
output:
"qc/{sample}_fastqc.html"
params:
sample_name=lambda wildcards: wildcards.sample
wildcard_constraints:
sample="[A-Za-z0-9_]+"
shell:
"fastqc {input} -o qc/"
wildcard_constraints 用 Python 正则表达式过滤。这在你的目录里有多个同名但不同格式的文件时特别好用。我在处理肿瘤样本时,常遇到样本名里有中划线、点号,如果不加约束,可能会匹配到意外的文件。加上正则表达式后,pipeline 稳定多了。
下面这张图帮你理清规则、输入输出、通配符之间的关系:
5. 规则执行的顺序:谁先谁后?
Snakemake 不按书写顺序执行规则。它只看 output 的依赖关系。假设你有两个规则:
rule create_input:
output:
"data/raw.txt"
shell:
"echo 'data' > {output}"
rule process:
input:
"data/raw.txt"
output:
"results/final.txt"
shell:
"cat {input} > {output}"
如果你请求 results/final.txt,Snakemake 会先检查 data/raw.txt 是否存在,如果不存在就执行 create_input。也就是说,规则执行顺序由文件依赖决定,而非代码顺序。这点和 Make 类似,但比 Make 更智能——Snakemake 能自动处理通配符的匹配。
我还见过一种坑:如果你在一个规则里修改了某个文件,但这个文件又被其他规则引用,Snakemake 不会自动重新运行规则,除非你显式声明了 run 或 shell 里的输出变化。我建议你养成习惯,每个规则只生成自己 output 里声明的文件,不要“顺便”写一些别的文件。
6. 归纳总结:规则、输入输出、通配符的关系
为了方便记忆,我把它们的核心点整理成表:
| 元素 | 作用 | 注意事项 |
|---|---|---|
| rule | 定义一个工作任务单元 | 名字唯一;可有多个同名规则?不行,必须唯一 |
| input | 声明规则所需的文件(支持命名、列表、glob) | 路径相对于 Snakefile 所在目录;通配符必须能在 output 中找到 |
| output | 声明规则生成的文件(必须写全,不能有重复) | 不能使用 glob 或通配符以外的模式;output 文件是规则的“契约” |
| 通配符 {x} | 匹配变化的样本名、染色体名等,实现批量化 | 命名必须一致;可用 wildcard_constraints 约束格式 |
| shell / run | 实际执行命令(shell)或 Python 代码(run) | shell 里用 {input}、{output} 引用;run 里用 input 和 output 变量 |
实用技巧: 当你调试通配符匹配时,可以在 Snakefile 开头加一句 print(wildcards) 查看当前匹配到的值。或者在 shell 命令里用 echo "sample is {wildcards.sample}" 输出日志,方便检查。
至此,你已经掌握了 Snakefile 最基本的三个要素。往后我们会在规则里加参数、日志、线程、资源分配等更高级的东西,但地基就是今天这些。我个人认为,你如果能熟练写出带通配符的多规则 Snakefile,Snakemake 的 80% 能力你已经到手了。
嗯,今天就到这里。记住:规则定义要清晰,输入输出要完整,通配符要命名一致。这三点做好,你的工作流就很难出错。