4. 第一个Snakemake工作流:编写简单规则,运行snakemake
好,我们正式开始动手写代码了。
前面讲了那么多概念,什么规则、输入输出、DAG图,说白了都是纸上谈兵。这一节我们要从零搭建一个真正能跑起来的Snakemake工作流。
我自己的习惯是,学任何新工具,第一个例子一定要简单到不能再简单。不是为了炫技,而是为了确认环境没问题、流程走得通。你想想看,如果在最简单的例子上就卡住了,后面复杂的分析肯定跑不下去。
4.1 从问题出发:我们想做什么
假设我们有这么个场景:
- 一个文本文件
data/input.txt,里面有几行文字 - 我们想把它复制一份,生成
results/copy.txt - 再统计一下原文件的行数,生成
results/stats.txt
就是这么简单。但别小看它——这个流程已经包含了工作流的两个核心要素:任务依赖和数据传递。
4.2 准备项目结构
先看看我们的文件该怎么组织。Snakemake本身对项目结构没有强制要求,但我是个有点强迫症的人——我习惯把数据、脚本、结果分开,这样看起来清爽,也好维护。
创建一个项目目录,比如 my_first_workflow/,在里面建好这些文件:
my_first_workflow/
├── Snakefile # 核心文件,定义规则
├── data/
│ └── input.txt # 输入数据
└── results/ # 输出目录(空文件夹)
输入文件 data/input.txt 的内容随便写几行就行,比如:
这是第一行
这是第二行
这是第三行
记得保存为纯文本格式,不要加什么特殊编码,UTF-8就够了。
4.3 编写第一个Snakefile
Snakefile就是Snakemake的配置中心,所有规则都写在这里。它是用Python语法扩展的,所以你可以在里面直接写Python代码——这一点我们后面会用到。
好,打开编辑器,创建一个空白文件命名为 Snakefile,开始写我们的第一条规则:
rule copy_file:
input:
"data/input.txt"
output:
"results/copy.txt"
shell:
"cp {input} {output}"
来,我们一起看一下这个规则的结构:
rule copy_file:—— 规则的名字,自己起的,但最好有意义input:—— 声明输入文件,一个或多个output:—— 声明输出文件,Snakemake靠这个来推断依赖顺序shell:—— 真正干活的命令,用{input}和{output}做占位符
4.4 再加一条规则,形成完整流程
光复制文件没什么意思,我们再加一个统计行数的规则:
rule count_lines:
input:
"data/input.txt"
output:
"results/stats.txt"
shell:
"wc -l {input} > {output}"
现在Snakefile里有两条规则了。但这里有个问题——这两条规则之间没有依赖关系,它们可以并行跑。如果我想要先复制、再统计,或者反过来,该怎么办?
答案很简单:通过输出文件建立起依赖链。我们修改一下:
rule count_lines:
input:
"results/copy.txt" # 这里变了!依赖copy_file的输出
output:
"results/stats.txt"
shell:
"wc -l {input} > {output}"
看到了吗?count_lines 的输入变成了 results/copy.txt,而 copy_file 的输出正是这个文件。Snakemake会自动识别这个依赖关系,先跑 copy_file,再跑 count_lines。
4.5 运行Snakemake
好了,Snakefile写完了,我们来看看怎么跑它。
打开终端,切换到项目目录 my_first_workflow/,然后输入:
snakemake --cores 1
--cores 1 表示用1个CPU核心。Snakemake默认是单核运行,你可以改成 --cores 4 来并行执行没依赖关系的任务。
如果一切顺利,你会看到类似这样的输出:
Building DAG of jobs...
Using shell: /bin/bash
Provided cores: 1
Rules claiming more threads will be scaled down.
Job stats:
job count min threads max threads
---------- ------- ------------- -------------
copy_file 1 1 1
count_lines 1 1 1
total 2 1 1
Select jobs to execute...
Execute 1 jobs...
[Mon Jan 15 10:00:00 2025]
rule copy_file:
input: data/input.txt
output: results/copy.txt
jobid: 0
resources: tmpdir=/tmp
Execute 1 jobs...
[Mon Jan 15 10:00:01 2025]
rule count_lines:
input: results/copy.txt
output: results/stats.txt
jobid: 1
resources: tmpdir=/tmp
Finished job 1.
1 of 1 steps (100%) done
注意看执行顺序:先跑了 copy_file,再跑 count_lines。这正是我们想要的效果。
现在检查一下 results/ 文件夹,应该有两个文件:
copy.txt—— 内容和input.txt一样stats.txt—— 里面写着3 data/input.txt(假设你写3行)
copy.txt 和 input.txt 的内容是否完全一致。虽然就是一次简单的cp操作,但亲眼看到结果和预期一致,这种成就感很重要。
4.6 再跑一次,看看Snakemake的聪明之处
现在试试第二次运行同样的命令:
snakemake --cores 1
你会发现输出变成了:
Building DAG of jobs...
Nothing to be done.
All jobs have been executed successfully.
为什么会这样?因为Snakemake发现所有输出文件都已经存在,而且它们的修改时间比输入文件要新。换句话说——没必要重复执行。
这其实就是Snakemake的增量计算能力。你想想看,在真实项目中,一个数据分析流程可能要跑几个小时甚至几天。如果你只是修改了一个输入文件,或者改了一小段代码,Snakemake只会重新运行受影响的那部分规则,而不是从头到尾全跑一遍。
4.7 本章核心知识图谱
下面这个图,展示的就是我们刚才搭建的工作流的完整逻辑。花一分钟看清楚,后面所有的复杂流程都是在这个基础上扩展的。
4.8 常见问题与避坑
写第一个Snakemake工作流时,有几个地方特别容易出错,我帮你提前扫个雷。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
报错 Missing input files |
输入文件路径写错了,或者文件不存在 | 检查路径大小写、是否在正确目录下 |
| 输出文件没生成 | shell命令写错了,或者输出路径写错了 | 直接在终端跑一下命令,确认能正常运行 |
| 规则执行顺序不对 | 输入输出依赖没建立起来 | 检查 input: 是否指向了上游规则的 output: |
| 第二次运行说"Nothing to be done" | 输出文件已经是最新的了 | 删除输出文件或用 -f 强制重新运行 |
snakemake -n 来做个"干跑"。它不会真的执行命令,但会打印出它计划要运行哪些规则、按什么顺序。我第一次写复杂工作流时,至少干了5次才敢真跑。
嗯,到这里,你的第一个Snakemake工作流就算完成了。虽然只是复制文件和统计行数,但你已经掌握了最核心的机制——规则定义、输入输出声明、依赖性解析、增量运行。后面的章节,我们会在这个基础上不断加东西,直到能跑完整的Seurat分析流程。