工作流基本概念:规则、输入、输出与运行命令

终于要动手写Snakemake了。别急,我们先搞懂四个最核心的概念——规则、输入、输出和运行命令。说白了,Snakemake就是一个聪明的流水线管理员,你告诉它“我想要什么”,它自己琢磨“需要什么原料”和“怎么生产”。

规则(rule):工作流的最小单元

规则是Snakemake的积木块。每个规则定义了一个步骤:拿什么东西,做什么处理,产出什么结果。我记得第一次写Snakemake时,把整个质控流程写在一个规则里,结果调试起来特别痛苦。后来才明白——一个规则只做一件事,这是铁律。

来看一个最简单的规则结构:

rule fastqc_raw:
    input:
        "data/sample1.fastq"
    output:
        "results/fastqc_raw/sample1_fastqc.html"
    shell:
        "fastqc {input} -o results/fastqc_raw"

这个规则叫fastqc_raw。它要吃一个FASTQ文件,然后吐出一个QC报告。Shell部分就是实际要敲的命令。{input}和{output}是Snakemake留好的变量,自动替换成你定义的文件名。

命名建议:规则名要有意义。我习惯用“处理环节+样本标识”的命名方式,比如trim_adapters、align_bwa。项目大了你就知道好处了——报错时一眼能定位是哪一步出了问题。

规则之间靠输入输出串联。你可以把B规则需要的输入,写成A规则的输出。Snakemake会自动梳理依赖关系。嗯,这就是工作流自动化的精髓。

输入(input):明确你要喂什么

input告诉Snakemake:“嘿,运行这个规则前,先把这些文件准备好。”它可以是单个文件,也可以是多个:

rule multi_input_demo:
    input:
        read1 = "data/sample1_R1.fastq",
        read2 = "data/sample1_R2.fastq",
        adapter = "config/adapters.fa"
    output:
        "results/trimmed/sample1_trimmed.fastq"
    shell:
        "cutadapt -a {input.adapter} "
        "-o {output} {input.read1} {input.read2}"

看到没?我给输入文件起了名字(read1、read2、adapter),然后在shell里用{input.名字}引用。这样代码可读性高得多。我在项目中见过有人把所有输入堆在一起,后来自己都分不清哪个是哪个——千万别学。

核心原则:输入文件必须存在,或者能被其他规则生成。Snakemake启动时会检查这条链,缺一个就报错。这其实是个好事——能帮你提前发现数据准备的问题。

输出(output):工作流的目标

output定义了规则要产出的文件。Snakemake用输出文件来判断“这个规则需要运行吗”。如果输出文件已经存在,且比输入文件新,它就跳过——这叫增量构建。

rule skip_if_exists_demo:
    input:
        "data/sample1.fastq"
    output:
        "results/fastqc_raw/sample1_fastqc.html"
    shell:
        "fastqc {input} -o results/fastqc_raw"

第二次运行时,如果sample1_fastqc.html还在,Snakemake直接跳过。这在质控场景里特别实用——你改了一个样本的数据,只重新跑跟它相关的规则,其他不动。

注意陷阱:输出文件不要手动删改。Snakemake依赖文件的修改时间戳做判断。我曾经手贱用touch更新了一个输出文件的时间戳,结果整个下游流程全被触发了,浪费了一个多小时。嗯,血的教训。

运行命令(shell/run/script):真正干活的

Snakemake提供三种方式写命令体:

方式 适用场景 示例
shell 直接执行Shell命令或调用外部工具 fastqc、cutadapt、bwa mem
run 执行内嵌Python代码 数据处理、格式转换、统计计算
script 运行独立的R/Python脚本文件 复杂的统计分析、绘图、报告生成

看个run的实例:

rule count_reads:
    input:
        "data/sample1.fastq"
    output:
        "results/counts/sample1_count.txt"
    run:
        total = 0
        with open(input[0]) as f:
            for line in f:
                if line.startswith('@'):
                    total += 1
        with open(output[0], 'w') as out:
            out.write(f"Total reads: {total//4}\n")

script方式我最常用,尤其是质控报告那一步:

rule generate_qc_report:
    input:
        fastq = "data/sample1.fastq",
        json = "results/multiqc/sample1_data.json"
    output:
        "results/reports/sample1_qc.html"
    script:
        "scripts/report_qc.R"

我的建议:能用shell就别用run,能用run就别用script。为什么?shell最直接,不引入额外依赖。只有遇到复杂的逻辑处理时,再考虑Python或R脚本。这是我经历过几个大项目后总结出来的——脚本太多,环境配置就成了新的噩梦。

画一张图,看明白四个概念的关系

下面这张图总结了规则、输入、输出和运行命令是怎么配合的。Snakemake的哲学就是:定义清楚“我要什么”和“我有什么”,中间的逻辑它自己搞定。

Snakemake工作流——四个基本概念的关系 输入(Input) 原始数据 / 中间文件 规则(Rule) 定义处理步骤 命名 → input → output → shell/run/script 输出(Output) 结果文件 / 报告 运行命令 shell / run / script 依赖 生成 调用 增量构建:输出已存在 → 跳过 工作流运行逻辑 ① Snakemake从最终目标(最终输出)出发,反向查找依赖 ② 如果某个输出不存在或过期,就执行对应的规则 ③ 遇到输入文件不存在,继续向前寻找能生成它的规则

再次强调:Snakemake是一个按需驱动的调度器。你给它一个最终目标,它自己倒推出所有需要跑的规则。中间哪个文件已经有了、不需要重新跑,它也门儿清。这就是自动化批次质控的基础——不用你操心,它自己搞定依赖和增量更新。

实战中的避坑经验

聊几个我实际踩过的坑,帮你省点时间:

  1. 目录要提前建好——Snakemake不会自动创建输出目录。我习惯在每个规则的shell里顺手加一句mkdir -p $(dirname {output}),或者在Snakefile开头统一建目录。
  2. input和output路径最好用绝对路径或相对项目根目录的路径——用os.chdir切换工作目录时相对路径会乱。我在一次跨服务器迁移时吃过亏,当时路径全乱了。
  3. 一个规则不要产生多个同名文件——Snakemake靠output的唯一性来映射依赖。重名输出会导致不可预料的跳过行为。
  4. shell命令里的引号要当心——Snakemake用Python解析shell字符串,单引号嵌套容易出问题。我建议shell命令里统一用双引号。

规则、输入、输出、运行命令——这四个概念构成了Snakemake的全部骨架。接下来几章,我们会把这些积木搭成真正的质控流水线。到时候你会发现,理解了这四个东西,剩下的都是锦上添花。