4. 通配符与模式匹配:单通配符、多通配符、glob通配、规则中的wildcards对象
通配符,说白了就是Snakemake能理解的一种模糊匹配规则。你告诉它“我要处理所有样本”,它就知道去找sample1、sample2、sample3……而不是傻傻地只认一个文件名。
我刚开始用Snakemake那会儿,最头疼的就是通配符。明明写对了,跑起来就是报错。后来我发现,99%的问题都出在匹配逻辑上。今天咱们把它彻底理清,以后你写规则,想怎么模糊就怎么模糊。
4.1 单通配符:最简单的模糊匹配
单通配符就是一个花括号包起来的变量名。比如{样本名}、{染色体}、{日期}。Snakemake看到这个,就知道要用实际的值去替换它。
核心语法:规则中定义输出文件时,用 {变量名} 表示通配符。
看个例子。假设你有一个RNA-seq项目,每个样本都要单独处理:
rule samtools_sort:
input:
"{sample}.sam"
output:
"{sample}.sorted.bam"
shell:
"samtools sort {input} -o {output}"
当你执行 snakemake sampleA.sorted.bam,Snakemake会自动把{sample}替换成sampleA。然后去找sampleA.sam作为输入。
通配符解析规则:
- 从目标文件(最终输出)开始,逆向解析
- 一个规则里可以有多个通配符,但同一个通配符在不同文件中的值必须一致
- 通配符的值不能是空字符串
我曾经踩过一个坑:
注意文件名中的特殊字符 Snakemake通配符默认不支持斜杠"/",除非你用特殊语法。我有次样本名里带了斜杠,结果Snakemake把它当成了路径分隔符,整个目录结构全乱了。从那以后,我强制团队用下划线代替斜杠,世界清净了。
4.2 多通配符:一对多匹配更灵活
单通配符只能处理一个维度的变化。但实际项目里,你经常需要多个维度同时变化。比如处理不同样本的不同染色体:
rule extract_chrom:
input:
"data/{sample}.bam"
output:
"results/{sample}/{chrom}.vcf.gz"
shell:
"bcftools view {input} -r {chrom} -o {output}"
这意味着你可以同时遍历样本和染色体。执行 snakemake results/sample1/chr1.vcf.gz,它会自动解析出sample=1, chrom=chr1。
多通配符的使用原则:
- 每个通配符独立匹配,互不干扰
- 同一个规则中同名通配符必须匹配相同值
- 通配符的解析顺序:从左到右,从上到下
这里有个陷阱:
建议:通配符命名要有语义。我见过有人用{file}这种名字,结果三个月后自己都看不懂。多用{sample}、{chr}、{date}这类有含义的名字。你想想看,维护代码时少折腾多少脑细胞。
4.3 glob通配:批量处理文件
有时候你不知道目标文件具体叫什么,或者想一次性匹配某个目录下所有符合条件的文件。这时候就需要glob通配符——也就是 wildcard_constraints 结合Python的glob模式。
Snakemake默认的glob通配是 * 和 ?:
rule merge_chunks:
input:
expand("chunk_{chr}.vcf.gz", chr=["chr"+str(i) for i in range(1,23)])
output:
"all_chr_combined.vcf.gz"
shell:
"bcftools concat {input} -o {output}"
但如果你想要更精细的控制,可以用 wildcard_constraints 加正则:
rule process:
input:
"{sample}.txt"
output:
"{sample}.out"
wildcard_constraints:
sample="sample_[0-9]+"
shell:
"process.py {input} {output}"
这能确保 {sample} 只匹配类似 sample_001 这样的名字,而不是你随手写的一个文件名。
glob通配的使用场景:
- 批量处理同名不同后缀的文件
- 处理按编号存放的数据
- 结合Python字符串格式化生成文件列表
嗯,这里要注意:
注意性能问题 glob通配底层依赖文件系统扫描。如果目录下有几十万个文件,每次解析都会变慢。我之前有个项目,每次执行Snakemake都要等3分钟解析目录,后来改成先列出文件列表再传进去,速度提升了10倍。别等到卡了才优化。
4.4 规则中的wildcards对象
在shell命令之外,你还可以在Python代码块(run、script、input函数)里直接访问 wildcards 对象。它是个命名元组,你可以在input函数里根据通配符动态决定输入:
rule infercnv:
input:
lambda wildcards: f"data/{wildcards.sample}/cnv_calls.txt"
output:
"results/{sample}.infercnv.pdf"
run:
sample_name = wildcards.sample
cmd = f"Rscript infercnv.R --sample {sample_name}"
shell(cmd)
这里wildcards就是Snakemake自动创建的对象。它包含了当前规则解析出来的所有通配符。
wildcards对象的核心方法:
| 方法/属性 | 说明 | 示例 |
|---|---|---|
wildcards.sample |
获取样本名 | "sampleA" |
wildcards.keys() |
获取所有通配符名称 | ['sample', 'chr'] |
wildcards.items() |
获取键值对 | [('sample', 'sampleA'), ('chr', 'chr1')] |
wildcards.get("sample", "default") |
安全取值,可设默认 | "sampleA" |
我个人习惯在run块开头就先把关键通配符提取出来:
rule qc_report:
input:
"analysis/{sample}.bam"
output:
"reports/{sample}.qc.html"
run:
sample_name = wildcards.sample
print(f"Processing {sample_name}")
# 这里可以调用外部脚本
shell(f"fastqc {input} -o reports/ --extract")
这样代码更清晰,也好调试。你想想看,一旦你熟悉了这个模式,动态输入输出就是小菜一碟。
关于wildcards,有几个关键点需要记住:
总结:
- wildcards在run和shell块中都能用,但在shell块中以 {variable} 形式
- 通配符的值在规则的输出文件被解析前就确定了
- 用wildcard_constraints可以约束通配符的匹配模式
我曾经写过这样一个规则,需要在run块里同时用到多个通配符:
rule merge_replicates:
input:
lambda wildcards: expand(
"results/{wildcards.sample}/rep_{rep}.txt",
rep=range(1, int(wildcards.n_replicates)+1)
)
output:
"results/{sample}/merged.txt"
params:
rep_count = lambda wildcards: int(wildcards.n_replicates)
run:
with open(output[0], 'w') as out:
for i in range(1, params.rep_count + 1):
with open(f"results/{wildcards.sample}/rep_{i}.txt") as f:
out.write(f.read())
这个规则用一个通配符{n_replicates}动态决定要汇总多少个重复样本,再用{sample}指定样本。wildcards对象帮我们优雅地串联了所有信息。
记住这个图,它描述了数据从输入到输出,被通配符匹配、解析、最终生成结果的全过程。你写规则的时候,脑子里就过一遍这个流程,一般不会出错。