4、规则进阶:通配符的使用、多输出规则、日志文件配置

Snakemake 的规则一旦碰上多样本、多文件,就会变得特别有意思。说白了,通配符就是让规则能批量干活的核心。我刚开始学的时候,总觉得通配符像个黑魔法,后来踩过几次坑,才明白它其实挺朴素的——就是把样本名字当作变量,在输入输出里来回传递。

这一章我们聊三个进阶点:通配符怎么玩、多输出规则怎么写、日志怎么配。嗯,这三个东西在实际项目中几乎天天用,你掌握好了,写 Snakefile 就能省下一大半体力。

通配符:规则的灵魂

先看一个最简单的例子。假设你有一堆样本,每个样本都有一个原始测序文件 data/sampleA.fastq,你想把它们分别比对到参考基因组上。用通配符,规则可以这样写:

rule align:
    input:
        "data/{sample}.fastq",
        "reference/genome.fa"
    output:
        "results/{sample}.bam"
    shell:
        "bwa mem {input[1]} {input[0]} | samtools sort -o {output}"

看到 {sample} 了吗?它就是通配符。Snakemake 会从目标文件或 expand() 函数中自动推导出通配符的具体值。比如你执行 snakemake results/sampleA.bam,它就知道 sample=A,然后自动把 input 里的 {sample} 替换成 sampleA。

我个人习惯给通配符起有语义的名字,比如 {sample}、{chr}、{tool},而不是 {x}、{y}。这样你看到输出路径,一眼就知道是哪个样本、哪个步骤。我曾经在项目里用 {a}、{b} 命名,结果两个月后自己都看不懂哪个对哪个,改起来特别痛苦。

通配符的约束:让规则更可控

如果通配符的值范围很大(比如所有样本名字),你可以在规则里用 wildcard_constraints 来限定它的格式:

rule align:
    input:
        "data/{sample}.fastq"
    output:
        "results/{sample}.bam"
    wildcard_constraints:
        sample = "[A-Za-z0-9_]+"
    shell:
        "bwa mem {input} > {output}"

这样做的好处是:

  • 避免通配符匹配到不该匹配的路径(比如含斜杠、点等特殊字符)
  • 让规则更清晰,别人看代码就知道样本名的合法字符集
  • 有时能防止意外的文件覆盖

遇到跨路径的通配符(比如样本名里包含 sample 子目录),我建议你直接用 wildcard_constraints 把路径分隔符排除掉,否则 Snakemake 可能会把目录当成通配符的一部分,造成匹配混乱。

多输出规则:一个规则生成多个文件

很多生物信息学工具会同时输出多个文件。比如 GATK 的 HaplotypeCaller 会生成一个 GVCF 文件和一个索引文件;又或者一个质控步骤会产生 report.html 和 report.json。Snakemake 支持一个规则写多个输出文件,并且输出文件之间可以共享通配符。

写法很简单,把 output 写成一个列表:

rule quality_control:
    input:
        "raw/{sample}.fastq"
    output:
        "qc/{sample}/report.html",
        "qc/{sample}/report.json"
    shell:
        "fastqc -o qc/{sample} {input}"

注意,这里的两个输出文件都使用了同一个通配符 {sample}。当 Snakemake 需要生成 qc/sampleA/report.html 时,它自动推导出 sample = sampleA,然后检查或生成第二个输出文件 qc/sampleA/report.json。

重要提示: 如果多个输出文件中的通配符不一致,Snakemake 会报错。比如 output: "a/{x}.txt", "b/{y}.txt",它无法保证 {x} 和 {y} 的值有唯一关联。所以多输出规则中,所有输出文件最好共享同一套通配符。

我在项目中遇到过一种情况:工具输出文件名字固定,但样本名不同。比如 coverage_metrics.txt 和 variant.vcf,没有通配符可以串联。这时候我一般用 output: "results/{sample}/coverage_metrics.txt", "results/{sample}/variant.vcf" 来强制保持通配符一致。

多输出与 aggregation

多输出规则还经常用于聚合步骤。比如你想把所有样本的质控报告收集到一个表格里:

rule collect_qc:
    input:
        expand("qc/{sample}/report.json", sample=SAMPLES)
    output:
        "results/all_qc.json"
    run:
        # 读取所有 JSON,合并输出
        ...

这里 expand() 会生成所有样本 report.json 的列表,Snakemake 会自动推导需要先运行 quality_control 规则来生成这些文件。多输出与聚合搭配,能组成高效的 DAG 图。

日志文件配置:别让错误信息淹没在终端

你想想看,一个流水线跑几百个样本,如果某个样本报错,你总不能去终端翻几千行输出吧?配置日志文件,就是把每个规则的执行日志单独存到文件里,方便事后排查。

Snakemake 在规则里提供了 log 指令,用法和 input、output 类似:

rule align:
    input:
        "data/{sample}.fastq"
    output:
        "results/{sample}.bam"
    log:
        "logs/align/{sample}.log"
    shell:
        "bwa mem ref.fa {input} 2> {log}"

注意:log 文件不会被 Snakemake 视为输出文件,所以不会触发下游规则的输入依赖。它只是纯粹用来存日志的路径。如果某个规则失败了,你就可以直接去看对应的日志文件,而不是满屏刷屏。

我曾经犯过一个错误:把日志文件写到了 output 里。结果下游规则想依赖这个日志文件,导致 Snakemake 误以为日志是重要的中间产物,还试图用它来触发重新运行。嗯,所以记住:日志只走 log,别走 output。

日志文件的路径规范

我个人的习惯是:把日志放在一个独立的 logs/ 目录下,按规则名和样本名组织。比如:

logs/align/sampleA.log
logs/qc/sampleA.log
logs/variant_calling/sampleA.log

这样当你需要定位某个样本的某个步骤时,路径清晰可辨。如果项目规模很大,还可以按日期或批次再分一层:logs/2025-04/align/sampleA.log。

日志文件与重跑逻辑

Snakemake 默认不会因为日志文件的变化而重新运行规则——日志文件只写,不读取。但是,如果你在 shell 命令里把日志写在标准输出(> {log}),并且日志文件提前存在,Snakemake 会不会认为规则已经完成?答案是不会。`log` 文件不参与判据,`output` 文件才是核心。

避坑提醒: 别把 log 路径跟 output 路径写重叠。Snakemake 会优先把匹配 output 的通配符值传给 log,但如果某个 log 文件恰好跟某个 output 文件同名,Snakemake 的行为可能让你意外。我建议日志文件名加个 .log 后缀,跟输出文件区分开。

本章知识结构图

下面这张图帮你梳理了通配符、多输出、日志之间的关系,以及它们如何融入 Snakemake 的数据流:

Snakemake 规则进阶:通配符·多输出·日志 通配符 {sample} / {chr} / {tool} 由 output 文件自动推导 wildcard_constraints 限制格式 避免特殊字符导致歧义 多输出规则 output: report.html, report.json 共享通配符,保持一致性 配合 expand() 聚合下游 一个规则生成多个产物 日志配置 log: "logs/{sample}.log" 独立于 output,不影响依赖 按规则+样本组织路径 用 2> {log} 重定向错误 提供 变量 每个输出 可配日志 三个概念如何协作? 1. 通配符让规则能匹配任意样本或染色体,是 Snakefile 的“变量系统”。 2. 多输出规则允许一个 shell 命令产出多个文件,避免重复扫描。 3. 日志文件为每个规则独立保存错误和状态,方便调试与追溯。 三者组合,让你写一次规则即可应对成百上千样本的批量流水线。

我的小技巧: 刚开始练习时,你可以先写一个单样本的硬编码规则(不包含通配符),跑通后再把样本名替换成 {sample}。这样能确保命令本身没问题,避免通配符和命令错误混在一起排查。毕竟,调试 Snakemake 最烦的就是“不知道是通配符出事还是工具出事”。

三者搭配:实际案例

最后给一个完整的代码片段,把通配符、多输出、日志都串起来:

# 定义样本列表
SAMPLES = ["A", "B", "C"]

# 比对规则
rule bwa_align:
    input:
        fq = "data/{sample}.fastq",
        ref = "reference/genome.fa"
    output:
        bam = "bam/{sample}.bam",
        bai = "bam/{sample}.bam.bai"
    log:
        "logs/bwa_align/{sample}.log"
    shell:
        "(bwa mem {input.ref} {input.fq} "
        "| samtools sort -o {output.bam} && "
        "samtools index {output.bam}) 2> {log}"

# 聚合质控
rule collect_stats:
    input:
        expand("bam/{sample}.bam.bai", sample=SAMPLES)
    output:
        "stats/coverage_summary.txt"
    log:
        "logs/collect_stats/coverage_summary.log"
    shell:
        "echo 'Sample\tReads' > {output} && "
        "for bam in {input}; do "
        "  samtools stats $bam >> {output}; done 2> {log}"

这个例子中,bwa_align 规则输出了 BAM 和 BAI 两个文件,并且把 stderr 都定向到了日志。之后 collect_stats 规则通过 expand() 聚合所有样本的 BAI 文件,生成汇总统计。你运行 snakemake stats/coverage_summary.txt,Snakemake 会自动推导出所有中间文件,包括日志。

如果某个样本的比对失败了,你直接去 logs/bwa_align/ 目录下找对应的 .log 文件,就能看到错误原因,而不需要重跑整个流水线。我个人觉得,日志配置这个习惯,一旦养成了,就再也回不去了——太省心了。