通配符与动态文件:让流水线学会“举一反三”

好,咱们进入第三个主题。前两章我们搭好了环境,也跑通了第一个小例子。但说实话,那种一个输入对一个输出的流水线,只能算“静态脚本”,算不上真正的流水线。

你想想看,一个科研项目通常要处理几十上百个样本。如果每个样本都手写一条规则,那得写到什么时候去?

所以这一章,我们来聊聊通配符(wildcards)。它是Snakemake最核心的机制之一。说白了,就是让你的规则学会“举一反三”——用一套规则,处理所有同类文件。

通配符是什么?

通配符是一个占位符。它代替文件名中会变化的部分。你可以把它理解成一个“变量”,但这个变量是从输入或输出文件名的模式中自动提取出来的。

举个最简单的例子。假设你有一堆测序数据,文件名是:

sample_A_R1.fastq
sample_A_R2.fastq
sample_B_R1.fastq
sample_B_R2.fastq
sample_C_R1.fastq
sample_C_R2.fastq

文件名里有三个部分会变:sample_A、sample_B、sample_C,还有R1和R2。但文件名结构是一样的。为此,我们可以写成:

rule fastqc:
    input:
        "data/{sample}_{read}.fastq"
    output:
        "results/fastqc/{sample}_{read}_fastqc.html"
    shell:
        "fastqc {input} -o results/fastqc/"

注意这个 {sample} 和 {read},就是通配符。它们被花括号包围,Snakemake会自动识别。

当我执行这条规则时,Snakemake会去查看output或input中哪些部分被通配符占了,然后从实际的文件路径中把值“抠”出来。比如:

# 假设你要生成 results/fastqc/sample_A_R1_fastqc.html
# Snakemake 解析后得到:
# sample = "sample_A"
# read = "R1"

然后它会把这个规则应用到所有匹配的文件上。一次写入,四处生效。

关键点:通配符的名字不重要,重要的是它在input和output中必须保持一致。Snakemake靠名字来建立输入和输出间的对应关系。

命名模式:怎么把“变量”传下去

通配符本身只是一个名字,真正让它发挥作用的是“命名模式”。这是一套约定——你怎么给通配符起名,怎么把它们串联起来。

我个人觉得,最难把握的反而是命名。我有一次在一个项目中用了 {s}、{r} 这种缩写,结果三天后自己都看不懂了。所以我的建议是:

  • 全小写字母:不用驼峰,不用下划线开头。
  • 含义清晰:用 {sample_id} 而不是 {s}。
  • 避免歧义:不要在同一规则的input和output里用相同命名但不同的通配符值。Snakemake会报错。

来看一个稍微复杂点的例子。假设我想对每个样本做质控,然后比对到参考基因组:

rule bwa_align:
    input:
        r1 = "data/{sample}_R1.fastq",
        r2 = "data/{sample}_R2.fastq",
        ref = "genome/hg38.fa"
    output:
        "results/bam/{sample}.sorted.bam"
    shell:
        "bwa mem {input.ref} {input.r1} {input.r2} | "
        "samtools sort -o {output}"

这里只有 {sample} 一个通配符。Snakemake会提取样本名,然后自动把R1和R2配对。这个模式很常见,我建议你记住它。

技巧:如果同一个流水线中出现多个通配符,尽量让它们的顺序保持一致。比如始终是 {sample}_{lane}_{read},不要在某些地方写成 {read}_{sample}_{lane}。

动态文件名匹配:Snakemake怎么知道该用哪个通配符

这里有个很精妙的设计:Snakemake不做“向前匹配”。它不会先读input,再去猜output。具体流程是这样的:

  1. 用户指定一个目标文件(通常是最终产出),比如 snakemake all 里面的目标。
  2. Snakemake 开始反向推导——它看你想要 results/bam/{sample}.sorted.bam 中的某个具体文件,比如 sample_A.sorted.bam。
  3. 从output路径中提取通配符:{sample} = "sample_A"。
  4. 把这个值代入input:data/sample_A_R1.fastq 和 data/sample_A_R2.fastq。
  5. 检查这些文件是否存在。如果存在,执行;如果不存在,Snakemake 会继续向上游找规则。

这个过程叫“目标驱动式执行”。它很聪明,但有个坑——如果某个通配符出现了但无法在input中找到合法文件,Snakemake 会报错。这也是我在项目中踩过最多的坑之一。

曾经踩过的坑:我在一个项目中把output写成了 "results/{sample}_R1_fastqc.html",但input里用的是 {sample}_{read}。结果Snakemake死活找不到文件,一直报“MissingInputException”。后来发现是output中的通配符顺序跟input不一致,导致Snakemake无法正确对位。

expand():批量生成目标文件

手动一个一个写目标文件太累了。Snakemake 提供了一个 expand() 函数。它的作用和Python的列表推导类似,但专门用来处理通配符。

# Snakefile
SAMPLES = ["A", "B", "C"]
READS = ["R1", "R2"]

rule all:
    input:
        expand("results/fastqc/{sample}_{read}_fastqc.html",
               sample=SAMPLES, read=READS)

这段代码会自动生成9个目标文件(3个样本 × 2个read = 6个,嗯,我算错了?其实是6个)。它会遍历所有组合。如果你想避免重复,可以传入一个列表的列表,或者使用 zip 模式。

# 配对模式:只生成 A_R1, B_R2
expand("results/fastqc/{sample}_{read}_fastqc.html",
       zip, sample=SAMPLES, read=READS)

用 zip 模式时,SAMPLES 和 READS 必须等长。它会把列表中的对应元素配对起来。我建议你在处理双端测序数据时用这个模式——只做R1和R2配对,不做R1+R1这种无意义组合。

一张图理解通配符流程

下面这张图展示了Snakemake如何通过通配符实现从原始数据到产出结果的自适应路径解析。核心是:output决定了通配符的值,input根据通配符的值去匹配文件。

Snakemake 通配符解析流程 data/sample_A.fastq 实际文件名(具体值) data/sample_B.fastq 实际文件名(具体值) data/sample_C.fastq 实际文件名(具体值) Snakemake 规则 input: "data/{sample}.fastq" output: "results/{sample}.out" 通配符:{sample} 提取 self->A 提取 self->B 提取 self->C results/A.out 通配符展开结果 results/B.out 通配符展开结果 results/C.out 通配符展开结果 Snakemake 从 output 中的实际文件名提取通配符值 然后代入 input 模式,找到对应的原始数据文件

常用模式对比

下面这张表整理了Snakemake中最常用的几种通配符使用模式。我在项目里基本只用这几种,够用了。

模式描述示例
{wildcard}单一通配符,用于单样本处理{sample}.bam
{w1}_{w2}多通配符,用下划线或其他固定字符分隔{sample}_{read}.fastq
expand()批量生成目标文件列表expand("res/{s}_{r}.txt", s=SAMPLES, r=READS)
expand(... zip)按位置配对生成,不交叉expand("res/{s}.txt", zip, s=LI)
通配符约束限定通配符匹配的范围,用正则{sample,[A-Z]+}

小提示:如果你发现某个通配符匹配了不该匹配的文件(比如 sample_A_R1.fastq 被 {sample} 匹配成了 sample_A_R),可以用通配符约束来解决。约束写在花括号里,用逗号隔开:{sample,\w+}。

避坑指南

最后,总结几个我踩过的坑。你写Snakefile的时候可以留意一下:

  • 通配符名一致严格:input和output中的通配符名字必须完全一样,大小写敏感。写错一个字母,就匹配不上了。
  • 不要滥用通配符:如果input中有通配符,output中也必须有。反之亦然。Snakemake不会自动为你创建不存在的通配符。
  • 路径后缀别写反:output的路径后缀通常是最终产出文件的格式(如.bam),而input的后缀是原始数据格式(如.fastq)。
  • 用 --cores 跑多线程时注意通配符解析:通配符是在执行前就解析好的。它不影响并行度。

曾经踩过的最隐蔽的坑:我在一个规则里用了两个通配符,但input中的顺序是 {group}_{sample},output中的顺序是 {sample}_{group}。Snakemake在解析时并没有报错,但生成的文件名完全乱了。后来我强制要求自己保持通配符顺序一致性,这个问题再也没出现过。