输入与输出文件:文件通配符与 Wildcards 实战

刚接触 Snakemake 那会儿,我天真地以为每条规则只能一次处理一个文件。

后来我接了个 RNA-seq 项目,四十几个样本,每个样本有双端测序的两个 fastq 文件。如果为每个样本写一条规则……那画面太美我不敢看。

还好,Snakemake 提供了通配符(Wildcards),这玩意儿就是批量工作流的灵魂。少写几十行重复代码,一个规则管所有样本。

到底什么是通配符?

说白了,花括号 {} 就是通配符的标记。你在 input 或 output 的文件路径里写 {sample},Snakemake 就会自动把 sample 当作一个变量,从实际文件路径里提取出具体的值。

举个例子:

rule align:
    input:
        "data/{sample}_R1.fastq",
        "data/{sample}_R2.fastq"
    output:
        "results/{sample}.bam"
    shell:
        "bwa mem ref.fa {input[0]} {input[1]} > {output}"

当你让 Snakemake 生成 results/Sample1.bam 时,它会自动把 {sample} 匹配成 Sample1。然后 input 里的 {sample} 也变成 Sample1,所以它去找 data/Sample1_R1.fastq 和 data/Sample1_R2.fastq。

这里有个关键点:通配符**必须**同时出现在 input 和 output 中(至少 output 里要有)。

你想想,output 里写了 {sample},Snakemake 才知道要匹配哪个值;如果 input 里也用了同样的通配符名,它就能把值传进去。如果 input 里没有,那这个通配符就只是 output 里的一个占位符,不算真正意义上的通配符——不过这种用法偶尔也用在生硬的文件名重命名上,我一般不推荐。

通配符的约束:别让它乱匹配

实际项目中,文件名可能长这样:

data/Sample1_L001_R1.fastq
data/Sample1_L002_R1.fastq
data/Sample1_L001_R2.fastq
data/Sample1_L002_R2.fastq

如果你只写 {sample}_L001_R1.fastq,Snakemake 默认通配符会尽量匹配最短的字符串,但它也可能匹配到 Sample1_L001 这种过长的东西——然后就会找不到对应的 input 文件。

这时候就需要 wildcard_constraints 来限定通配符的格式:

rule align:
    input:
        "data/{sample}_L001_R1.fastq",
        "data/{sample}_L001_R2.fastq"
    output:
        "results/{sample}.bam"
    wildcard_constraints:
        sample = "[a-zA-Z0-9]+"   # 只允许字母和数字
    shell:
        "bwa mem ref.fa {input[0]} {input[1]} > {output}"

你可以把它理解成正则表达式的小检查。我有个习惯:只要通配符前后还有固定字符(比如 _L001),我都会加约束,避免匹配越界。

多个通配符的协同

一个规则当然可以同时用多个通配符。比如你要按样本和测序 Lane 来拆分输出:

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

当你请求 trimmed/Sample1_L001_R1.fastq 时,sample 被匹配成 Sample1,lane 被匹配成 L001。Snakemake 自动推导出所有依赖。

这里有一个坑:多个通配符同时出现时,Snakemake 会尝试用**贪婪匹配**,但它会用默认的 [^/]+ 来匹配每个通配符。如果固定字符太少,匹配可能出错。

我曾经遇到过这样一条规则:

output: "results/{sample}_{date}.txt"

文件名是 results/Sample1_20250101.txt,Snakemake 把 sample 匹配成 Sample1,date 匹配成 20250101,没问题。但如果文件名里有下划线,而固定字符又区分不清,就需要用 wildcard_constraints 分别约束:

wildcard_constraints:
    sample = "[A-Za-z0-9]+",
    date = "\\d{8}"

通配符在 input 中的特殊写法:函数形式

有时候 input 文件不是简单的字符串拼接,而是需要根据样本 ID 去查一个映射表。比如样本名是 SRR123456,但输入文件存在 fastq/SRR123456_1.fastq.gz。

你可以写一个 Python 函数:

def get_fastq(wildcards):
    return f"fastq/{wildcards.sample}_1.fastq.gz"

rule mapping:
    input:
        r1 = get_fastq
    output:
        "aligned/{sample}.bam"
    shell:
        "bwa mem ref.fa {input.r1} > {output}"

这里的 wildcards 是一个对象,包含当前规则的所有通配符值。你可以用 wildcards.sample 拿到当前样本名。这种写法在处理样本名称和文件名不一致时特别有用。我经常把它用于样本名到 FTP 路径的转换。

常见错误与避坑指南

我收集了几个初学者最容易摔跤的地方:

错误现象原因解决方案
MissingInputException通配符在 output 里,但 input 里没写同名的通配符检查 input 路径是否包含相同的 {wildcard}
WildcardError: no wildcard in output你给规则的目标文件是绝对路径,没有通配符确保 output 里至少有一个花括号变量
匹配到了多余字符固定字符和通配符边界模糊加 wildcard_constraints 或者用 _ 等分隔符固定
多个通配符解析混乱通配符之间没有足够固定的分隔字符使用 wildcard_constraints 分别限定格式

小技巧:调试通配符时,可以在规则里加一行 shell: "echo {wildcards}" 先跑一下,看 Snakemake 到底把通配符解析成了什么值。

注意:通配符名不能包含点号 . 或连字符 -,只能用字母、数字和下划线。我见过有人写 {sample.file},结果抛语法错误。老老实实用下划线分隔,比如 {sample_file}。

通配符的匹配流程(一张图说明)

我觉得用一张图来解释通配符怎么把目标文件拆成 input 最直观。下面是我画的:

Snakemake 通配符匹配流程 请求文件 results/{sample}.bam 提取通配符 {sample} → 实际值: Sample1 将值代入 input 通配符 data/{sample}.txt → data/Sample1.txt (同时约束 wildcard_constraints 过滤) 执行规则 输出 results/Sample1.bam

核心要点:通配符就是 Snakemake 的「万能钥匙」。你只需定义一次规则、写一次命令,Snakemake 会自动用通配符匹配出所有组合。记住 output 里的通配符是驱动者,input 里的通配符是被动继承者。

总结一下通配符的常用模式

场景示例说明
单样本处理output: "results/{sample}.bam"通配符 {sample} 从目标文件名提取
双端数据output: "results/{sample}_{lane}.bam"两个通配符,分别对应样本和Lane
限制匹配范围wildcard_constraints: sample = "\\d+"只匹配数字样本ID
动态input函数input: lambda wildcards: f"path/{wildcards.sample}.txt"Python Lambda 灵活构造路径

你想想看,如果没有通配符,每个样本写一条 rule 是什么感觉?

我当年第一个 Snakemake 工作流写了 200 多行,后来用通配符重写,直接缩到 40 行。嗯,这才是自动化该有的样子。