第一个 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 会疯掉的。

小贴士:Snakefile 里不要写中文注释以外的中文字符,特别是路径。我曾经在 Windows 上遇到过一个诡异的 Unicode 问题,折腾了俩小时才发现是文件名里有个全角空格。

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,而不是裸文件名。要不然工作目录一换,全完蛋。

常见坑:input 和 output 的路径是相对于 Snakefile 所在目录的。如果你在子目录里运行 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 图展示了一个简单的两条规则的工作流:

两个规则的 Snakemake 工作流结构 raw.fastq rule fastqc input: raw.fastq raw_fastqc.html rule multiqc input: *.html multiqc_report.html 箭头表示依赖关系:规则2 依赖 规则1 的输出

你看,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 脚本里,然后在 shell 里只调脚本。这样 Snakefile 更干净,脚本还能单独测试。

3.7 本阶段总结

好了,我们现在已经写出了第一个 Snakefile。别看它简单,结构上已经包含了核心要素:规则名字、input、output、shell/run。用这个框架你可以搭建任意复杂的工作流骨架。

我在教学的时候发现,很多人刚开始只记住了语法,但忽略了“Snakemake 是靠 output 文件来判断是否重新执行”这个逻辑。你想想,假如你改了输入文件但输出文件名没变,Snakemake 会认为输出已经是最新的,直接跳过。这时你需要手动删除旧输出,或者用 snakemake --force 强制重新跑。