第一个 Snakefile:从零搭起你的第一条规则
还记得我第一次用 Snakemake 时,对着那个什么都能干的 Makefile 风格发了半天呆。说起来挺不好意思的——我花了一整天才整明白,原来规则(rule)才是整个工作流的灵魂。你别笑,后来我带团队的时候,至少一半新人都在这一步卡过。
好,今天我们就来写第一个 Snakefile。别把它想复杂了。说白了,Snakefile 就是一个文本文件,里面写了一条或者多条规则。每条规则告诉 Snakemake:“喂,我想从 A 变成 B,你帮我跑这个命令”。
3.1 一个最简单的规则长什么样
我们直接来看代码。这几乎是 Snakemake 的“Hello World”了:
rule hello:
output:
"hello.txt"
shell:
"echo 'Hello, Snakemake!' > hello.txt"
你看,规则由 rule 关键字开头,后面跟规则名字 hello。然后是一个冒号,下面缩进写字段。这个规则里只写了 output 和 shell 两个字段。
- output:规则要生成的文件。Snakemake 会根据 output 判断规则有没有完成。
- shell:实际执行的 shell 命令。你可以写任何 bash 能跑的东西。
我习惯把规则名起得有意义一点,比如 trim_raw_reads 而不是 rule1。你想想,等到工作流膨胀到几十条规则,看到 rule1 会疯掉的。
3.2 保存文件,跑起来
把上面的代码复制进一个叫 Snakefile(注意没有后缀)的文件里,放在你的工作目录。然后打开终端,运行:
snakemake -c1
-c1 表示使用 1 个 CPU 核心。Snakemake 会读取当前目录下的 Snakefile,找到名为 hello 的规则,检查 hello.txt 是否存在:
- 如果不存在 → 执行
shell里的命令,生成文件。 - 如果已经存在 → 什么都不做,告诉你“nothing to be done”。
我第一次看到那个“nothing to be done”还以为报错了。其实不是,Snakemake 的意思是这个文件已经是最新的,不需要重新生成。嗯,这里要注意:它只检查文件存不存在,不看文件内容有没有变化——除非你用了 input 字段。
3.3 让规则有输入
真正的工作流里,规则不光有输出,还得有输入。比如你要对原始 FASTQ 文件做质控,输入是 sample.fastq,输出是 sample_fastqc.html。
rule fastqc:
input:
"sample.fastq"
output:
"sample_fastqc.html"
shell:
"fastqc sample.fastq -o ."
input 字段声明了依赖。Snakemake 会确保输入文件存在,才会执行这个规则。如果输入文件本身是另一个规则的输出,Snakemake 会自动先跑那条规则——这就是工作流编排的核心。
我个人的经验:input 和 output 里的文件名最好用路径格式,比如 data/raw/sample.fastq,而不是裸文件名。要不然工作目录一换,全完蛋。
snakemake,记得用 --directory 参数指定 Snakefile 位置。
3.4 规则的完整结构
一个完整的 Snakemake 规则可以包含很多字段。我列一个常用的清单:
| 字段 | 作用 | 是否必选 |
|---|---|---|
name |
规则名称,跟在 rule 后面 |
是 |
input |
输入文件列表 | 否(但通常需要) |
output |
输出文件列表 | 是(至少一个) |
shell |
要执行的 shell 命令 | 与 run 二选一 |
run |
用 Python 代码执行 | 与 shell 二选一 |
params |
额外的参数 | 否 |
log |
日志文件路径 | 否 |
threads |
需要的线程数 | 否 |
resources |
内存、时间等资源 | 否 |
你会发现 input 居然不是必选的。没错,有些规则可以没有输入,比如我们一开始的 hello 规则。但质控工作流里,几乎每个规则都有输入。
3.5 Snakefile 的文件结构
Snakefile 本质上是一个 Python 增强的 Makefile。你可以在一份文件里写多条规则,Snakemake 会根据最终目标文件自动构建依赖图。下面这个 SVG 图展示了一个简单的两条规则的工作流:
你看,rule multiqc 的输入是 raw_fastqc.html,而后者正好是 rule fastqc 的输出。Snakemake 自动识别了这个依赖,会先跑 fastqc,再跑 multiqc。你只需要运行一次 snakemake -c1,它就知道顺序。
这里有个容易踩的坑:如果你改变了一个规则的名字,但输出文件名没变,Snakemake 可能会以为只是新加了一个规则,旧规则还在。我建议规则名和输出文件名保持逻辑一致,比如 rule trim_galore 就产出 trimmed.fastq,不要乱起名。
3.6 用 run 字段代替 shell
有时候你想在规则里运行 Python 代码,而不是 shell 命令。Snakemake 的 run 字段就是干这个的:
rule count_lines:
input:
"hello.txt"
output:
"line_count.txt"
run:
with open(input[0], "r") as f:
lines = len(f.readlines())
with open(output[0], "w") as f:
f.write(str(lines) + "\n")
在 run 块里,你可以直接用 Python 变量 input 和 output(它们是列表)。我个人更喜欢用 shell,因为大部分生信工具都是命令行程序。但当你需要做文件解析、数值计算时,run 会很顺手。
shell 里只调脚本。这样 Snakefile 更干净,脚本还能单独测试。
3.7 本阶段总结
好了,我们现在已经写出了第一个 Snakefile。别看它简单,结构上已经包含了核心要素:规则名字、input、output、shell/run。用这个框架你可以搭建任意复杂的工作流骨架。
我在教学的时候发现,很多人刚开始只记住了语法,但忽略了“Snakemake 是靠 output 文件来判断是否重新执行”这个逻辑。你想想,假如你改了输入文件但输出文件名没变,Snakemake 会认为输出已经是最新的,直接跳过。这时你需要手动删除旧输出,或者用 snakemake --force 强制重新跑。