第4章:Snakemake通配符与输入输出模式
要说Snakemake最让我上头的功能,就是通配符。没有它,你只能为每个样本写一条规则——想想50个Seurat样本怎么办?手写50个rule?别开玩笑了。通配符让一条规则匹配无数个文件,管道瞬间变得灵活。
4.1 通配符匹配:规则里的变量
通配符说白了就是规则里的占位符。你写个花括号包住名字,比如 {sample},Snakemake就会在运行的时候去匹配实际的样本名。
比如这条规则:
rule process_seurat:
input:
"data/{sample}/raw_counts.h5"
output:
"results/{sample}/seurat.rds"
shell:
"Rscript scripts/run_seurat.R {input} {output}"
这里 {sample} 就是通配符。当你请求输出文件 results/sampleA/seurat.rds 时,Snakemake 自动把 {sample} 设为 sampleA,然后去找输入 data/sampleA/raw_counts.h5。
通配符的约束机制
默认 {sample} 匹配任意字符(不包括路径分隔符),但有时候你得限制格式。比如样本名只能由字母和数字组成:
wildcard_constraints:
sample = "[A-Za-z0-9]+"
我在项目中遇到过这样的坑:样本名里有下划线,结果通配符把整个路径都吞了,导致规则匹配失败。后来加了约束,再也没出过这种问题。
4.2 多输入与多输出:一个规则管多个文件
单细胞管道里,一个样本往往有多个输入文件。比如两个样本的Seurat对象合并成一份:
rule merge_samples:
input:
s1 = "results/sampleA/seurat.rds",
s2 = "results/sampleB/seurat.rds"
output:
"results/merged/combined_seurat.rds"
shell:
"Rscript scripts/merge_seurat.R {input.s1} {input.s2} {output}"
多输出同理:
rule split_and_qc:
input:
"data/bulk_counts.csv"
output:
qc_report = "results/qc_report.html",
filtered = "results/filtered_counts.csv"
shell:
"Rscript scripts/qc_and_filter.R {input} {output.qc_report} {output.filtered}"
我个人习惯用命名输出,这样在后续规则里引用更清晰,不会搞混顺序。
扩展模式:用expand批量生成文件名
当你需要在输入里使用多个通配符的组合时,expand() 函数是你的好朋友。它的本质是笛卡尔积展开。
rule aggregate_results:
input:
expand("results/{sample}/{condition}/seurat.rds",
sample = ["A","B","C"],
condition = ["control","treat"])
output:
"results/all_aggregated.rds"
shell:
"Rscript scripts/aggregate.R {input} {output}"
这段代码会展开成:
results/A/control/seurat.rds
results/A/treat/seurat.rds
results/B/control/seurat.rds
results/B/treat/seurat.rds
results/C/control/seurat.rds
results/C/treat/seurat.rds
你想想看,如果不这样写,你得手动列6个文件,多麻烦。
expand() 默认用笛卡尔积,但你也可以指定组合方式。比如用 zip 模式实现一一对应:expand("results/{sample}/seurat.rds", zip, sample=["A","B"], condition=["ctrl","treat"]) 只会生成 A/ctrl 和 B/treat。
4.3 模式扩展:动态获取文件列表
很多时候你根本不知道样本列表是什么,比如今天新来了数据,明天又加了一批。这时候就得用 glob_wildcards() 自动扫描目录。
用glob_wildcards发现样本
假设你的原始数据都存在 data/ 下,每个样本有自己的子目录。你可以写一个rule来收集所有样本名:
SAMPLES, = glob_wildcards("data/{sample}/raw_counts.h5")
print(SAMPLES)
glob_wildcards 会扫描文件系统中匹配该模式的所有文件,然后提取 {sample} 的值。结果是一个元组,注意后面的逗号——这是Python的元组拆包语法,我一开始总忘。
另一种常见做法是用 lambda 函数动态构造输入:
rule all:
input:
lambda wildcards: expand("results/{sample}/seurat.rds",
sample=SAMPLES)
这样的好处是——数据目录变了,样本列表也跟着变,你根本不用改Snakefile。
4.4 避坑指南:我在哪里摔过跤
| 常见错误 | 表现 | 解决办法 |
|---|---|---|
| 通配符未在output中出现 | Snakemake报错:WildcardError | 确保所有通配符都在output里用过一次,并且是可推导的 |
| 通配符约束太宽 | 错误地匹配了不该匹配的目录 | 用wildcard_constraints限制正则表达式 |
| expand时样本名含特殊字符 | 路径解析异常 | 使用lambda或python函数做清洗 |
| glob_wildcards路径写错 | 匹配不到任何文件 | 检查路径开头是否有./或绝对路径,最好用Path对象 |
glob_wildcards("data/{sample}/*.h5"),结果样本目录下没有.h5文件,SAMPLES变成了空列表。后续规则全部报错。后来我改成先检查目录是否存在,再用glob_wildcards扫描子文件。记住:通配符匹配基于文件存在性,不要假设目录结构一定规整。
4.5 核心逻辑:通配符解析流程
下面这张图总结了Snakemake是怎么把一条带通配符的规则变成具体任务的。
这张图的流程很直白:先是用户请求一个输出文件,Snakemake 从输出文件名里提取出通配符的值,然后去文件系统里找对应的输入文件。找到了就实例化规则,找不到的话它会往上找上游规则。这就是 Snakemake 的 “反向依赖解析” 机制,通配符是它运转的齿轮。
- 通配符是规则的参数化入口,必须出现在输出路径中才能被解析
expand()是批量生成文件名列表的利器,配合glob_wildcards实现动态管道- 多输入多输出用命名方式更清晰(
{input.s1}) - 通配符约束可以避免意外的目录匹配——尤其是当样本名含点号或下划线时