通配符(wildcards)的使用:模式匹配、动态文件名、多重通配符

通配符这东西,说白了就是 Snakemake 里的“变量替身”。你写 Snakefile 的时候,很多文件名是未知的——样本名、染色体号、批次……靠硬编码?那不现实。我刚开始接触 Snakemake 时,就被通配符的灵活度震住了。它能让一个规则同时处理上百个文件,而且代码几乎不用改。

这一节,咱们就聊聊通配符的核心玩法:模式匹配、动态文件名,还有多重通配符。理解透这些,你的流程就能从“写死”变成“写活”。

模式匹配基础

Snakemake 通过 input 和 output 里的花括号 {...} 来识别通配符。比如:

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

这里的 {sample} 就是通配符。Snakemake 会从用户要生成的最终文件(比如 results/HCC1143.bam)中提取出 sample=HCC1143,然后替换到 input 中,变成 data/HCC1143.fastq。

个人习惯:我通常用带下划线的命名,比如 {sample_id}、{chrom},避免和 Python 关键字冲突。虽然 {sample} 没问题,但多字通配符还是加个分隔符更安全。

通配符匹配的是文件名中 除路径分隔符 / 以外的任意字符。如果你有嵌套文件夹,比如 data/{group}/{sample}.fastq,那 {group} 和 {sample} 会各自匹配路径中的一段。

来看看匹配规则表:

通配符写法匹配内容示例路径提取的值
{sample}单个文件夹/文件名片段(不含 /)data/HCC1143.fastqHCC1143
{chr}_{pos}两个通配符,用下划线分隔chr1_1000.vcfchr=chr1, pos=1000
{run}_{lane}.{sample}三个通配符混合run01_lane2.HCC1143.bamrun=run01, lane=lane2, sample=HCC1143

嗯,这里要注意:通配符不能包含路径分隔符,所以如果你的文件名里带了 /,Snakemake 会直接报错。解决办法是提前把路径拆成多个通配符,或者用 glob_wildcards 辅助。

动态文件名实战

动态文件名指的是:output 里包含通配符,但 input 的路径需要根据通配符进行转换或查找。最常见的是 依赖其他规则输出的文件名。举个例子:

rule trim:
    input:
        "data/{sample}_R1.fastq",
        "data/{sample}_R2.fastq"
    output:
        "trimmed/{sample}_trimmed_R1.fastq",
        "trimmed/{sample}_trimmed_R2.fastq"
    shell:
        "cutadapt -o {output[0]} -p {output[1]} {input}"

rule align:
    input:
        r1 = "trimmed/{sample}_trimmed_R1.fastq",
        r2 = "trimmed/{sample}_trimmed_R2.fastq"
    output:
        "bams/{sample}.bam"
    shell:
        "bwa mem ref.fa {input.r1} {input.r2} > {output}"

看到了吗?align 规则的 input 里也用了通配符 {sample},Snakemake 会自动从 output 中提取 sample 值,然后把它代入到 input 里,找到对应的 trim 输出文件。这就是动态文件名的精髓——通配符沿着依赖链自动传递。

核心机制:Snakemake 通过目标文件(target)解析通配符,然后反向推导所有上游输入。你不需要手动写循环,它自己会搞定。

但有个坑:如果同一个规则里有两个不同的通配符,但 input 里只出现一个,Snakemake 会报“unused wildcards”错误。举个例子:

rule bad_rule:
    input:
        "data/{sample}.txt"
    output:
        "results/{sample}_{type}.txt"  # 这里 type 没在 input 里出现
    shell:
        "cat {input} > {output}"

运行时会报 WildcardError: Wildcards in output not used in input: type。解决办法是让 input 也包含那个通配符,或者用 params 传递额外变量。

我曾经踩过的坑:写了一个多通配符的 output,但 input 只用到其中一个,结果排错花了两小时。我的建议:output 里的每个通配符,至少要在 input 或 params 中出现一次,否则 Snakemake 不知道该怎么填充它。

多重通配符与冲突处理

当你的 output 包含两个或以上通配符时,Snakemake 如何解析?它用 文件名匹配优先级:先固定文字部分,然后从左到右贪婪匹配。比如:

output: "results/{project}_{sample}.bam"
target: "results/ProjectX_Sample1.bam"

Snakemake 会将 ProjectX 匹配到 {project},Sample1 匹配到 {sample}。但如果文件名有歧义,比如 results/A_B_C.bam,它怎么分?它会先匹配第一个通配符实际上 Snakemake 使用 正则表达式 解析:{project} 匹配到 A,{sample} 匹配到 B_C?还是 A_B 和 C?

答案是:Snakemake 默认是 贪婪的从右向左匹配。最后一个通配符会尽可能多地吞字符,直到遇到下一个分隔符。所以 A_B_C 会被拆成 project = A_B,sample = C——因为分隔符是 _,而 {sample} 是最后一个通配符,它先从右边开始吞 C,剩下的 A_B 给 {project}。

这很容易出问题,所以我推荐的做法是:

  • 使用固定长度的分隔符或数字前缀,比如 {project}_{sample_id:08d}。
  • 使用 命名通配符约束,通过 wildcard_constraints 限定每个通配符的匹配模式。
wildcard_constraints:
    sample = "[A-Za-z0-9]+",
    project = "[A-Z]+"

rule example:
    output:
        "results/{project}_{sample}.txt"
    ...

这样 ProjectX_Sample1 就会明确匹配:project = ProjectX(符合 [A-Z]+),sample = Sample1(符合 [A-Za-z0-9]+)。如果遇到 ABC_abc123,也能正确解析。

另一种常见场景:同一个通配符多次出现,比如:

rule merge:
    input:
        "results/{sample}_part1.txt",
        "results/{sample}_part2.txt"
    output:
        "merged/{sample}.txt"

这没问题,通配符会自动保持一致——同一个 {sample} 在所有位置都会替换成相同的值。但如果你不小心写了大小写不同的名字,比如 {Sample} 和 {sample},Snakemake 会当成两个不同的通配符,然后可能因为缺失匹配而报错。

小技巧:在大型流程里,我会在 Snakefile 开头定义一个 samples 列表,然后用 expand() 生成所有最终目标文件,确保通配符一致性。比如:
SAMPLES = ["HCC1143", "HCC1954"]
rule all:
    input:
        expand("bams/{sample}.bam", sample=SAMPLES)
这样 Snakemake 会自动为每个样本实例化整个流程。

最后强调一下 多重通配符的冲突避免:

  1. 不同通配符之间用明显的分隔符(_、-、.),别只用数字或字母连写。
  2. 尽量让通配符数量少,能合并的就合并。比如 {project}_{sample}_{lane} 可以改成 {run_id},然后在 params 里解析。
  3. 使用 wildcard_constraints 给每个通配符“带上镣铐”。

来看一个完整的例子,把上面知识点串起来:

# Snakefile
wildcard_constraints:
    sample = "[a-zA-Z0-9]{4,10}",
    lane = "L00[1-8]"

rule all:
    input:
        "bams/merged/HCC1143.bam"

rule fastqc:
    input:
        "raw/{sample}_{lane}_R1.fastq",
        "raw/{sample}_{lane}_R2.fastq"
    output:
        "qc/{sample}_{lane}_fastqc.zip"
    shell:
        "fastqc {input} -o qc/"

rule align:
    input:
        r1 = "raw/{sample}_{lane}_R1.fastq",
        r2 = "raw/{sample}_{lane}_R2.fastq"
    output:
        "bams/{sample}_{lane}.bam"
    shell:
        "bwa mem ref.fa {input.r1} {input.r2} > {output}"

rule merge_lanes:
    input:
        bams = expand("bams/{sample}_{lane}.bam", lane=["L001","L002","L003"])
    output:
        "bams/merged/{sample}.bam"
    shell:
        "samtools merge {output} {input.bams}"

你看,通配符 {sample} 和 {lane} 在不同规则间自由传递,expand() 动态生成所有 lane 的文件列表,wildcard_constraints 则确保输入文件名符合预期格式。

下面这张图概括了通配符在 Snakemake 中的解析流程:

通配符解析与依赖链 目标文件(target) 提取通配符值 (从文件名中正则匹配) 将通配符代入 input 表达式 (替换 {sample}、{lane} 等) 解析上游规则与通配符约束 (检查 wildcard_constraints、冲突) 递归执行(或执行当前规则) 关键点: 通配符沿依赖链 自动向下传递 常见问题: 贪婪匹配冲突 → 用约束解决

通配符用得好,你的流程可以轻松适配不同数据集。用不好,你可能会陷入“为什么这里匹配错了”的困惑中。记住:明确分隔、约束先行、测试小提,这三条是我多年积累下来的口诀。希望你们少走我走过的弯路。