通配符与动态文件:让流水线学会“举一反三”
好,咱们进入第三个主题。前两章我们搭好了环境,也跑通了第一个小例子。但说实话,那种一个输入对一个输出的流水线,只能算“静态脚本”,算不上真正的流水线。
你想想看,一个科研项目通常要处理几十上百个样本。如果每个样本都手写一条规则,那得写到什么时候去?
所以这一章,我们来聊聊通配符(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。具体流程是这样的:
- 用户指定一个目标文件(通常是最终产出),比如
snakemake all里面的目标。 - Snakemake 开始反向推导——它看你想要
results/bam/{sample}.sorted.bam中的某个具体文件,比如sample_A.sorted.bam。 - 从output路径中提取通配符:
{sample} = "sample_A"。 - 把这个值代入input:
data/sample_A_R1.fastq和data/sample_A_R2.fastq。 - 检查这些文件是否存在。如果存在,执行;如果不存在,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中最常用的几种通配符使用模式。我在项目里基本只用这几种,够用了。
| 模式 | 描述 | 示例 |
|---|---|---|
{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在解析时并没有报错,但生成的文件名完全乱了。后来我强制要求自己保持通配符顺序一致性,这个问题再也没出现过。