2. Snakefile 基础:规则构成、输入输出与通配符

说实话,刚接触 Snakemake 的人,第一反应往往是:“这不就是个加强版 Makefile 吗?”

嗯,有点像,但核心逻辑完全不同。

Makefile 关注的是“如何编译”,Snakefile 关注的是“如何串联数据流水线”。

我刚开始用的时候,也踩过不少坑。今天我们就从最基础的规则开始,把骨架搭起来。

2.1 规则长什么样?

一个规则,说白了就是一个“加工指令”。

告诉 Snakemake:你要用什么原料,通过什么方式,做成什么成品。

最基本的结构就四部分:规则名、输入、输出、执行动作。

看这个例子:

rule clean_data:
    input:
        "data/raw_data.csv"
    output:
        "data/cleaned.csv"
    shell:
        "python scripts/clean.py {input} {output}"

规则名叫 clean_data。输入是原始 CSV,输出是清洗后的文件。

Shell 部分就是实际干活的那行命令。大括号里的 {input} 和 {output} 会自动替换成你定义的文件路径。

我个人习惯:把 input 和 output 对齐写,一条一行。缩进用四个空格,不要用 Tab。Snakemake 对 Tab 支持不好,我吃过这亏。

2.2 input 和 output:规则的核心纽带

Snakemake 的规则是靠文件依赖关系连接起来的。

A 规则的输出,可以成为 B 规则的输入。这样,流水线就串起来了。

关键点在哪?Snakemake 只关心“目标文件是否存在”。

比如你运行:

snakemake data/feature_engineered.csv

它会一路向上回溯,找到那个能产生这个文件的规则,然后检查所有上游依赖是否齐全。

看一个多步骤的例子:

rule normalize:
    input:
        "data/cleaned.csv"
    output:
        "data/normalized.csv"
    shell:
        "python scripts/normalize.py {input} {output}"

rule engineer_features:
    input:
        "data/normalized.csv"
    output:
        "data/feature_engineered.csv"
    shell:
        "python scripts/engineer.py {input} {output}"

engineer_features 依赖 normalize 的输出。normalize 又依赖 clean_data 的输出。

你只需要指定最终目标文件,Snakemake 自动帮你排好执行顺序。

这里有个坑:如果你不小心在规则的 output 里写了不存在的路径,Snakemake 不会报错,而是直接执行——但执行完后文件找不到,才报。我曾经因为这个浪费了两个小时。

2.3 通配符:解开重复规则的锁

上面写的规则,一次只能处理一个文件。

但在真实项目里,你可能有几十个原始数据文件,难道每个都要写一条规则?

当然不。这时候就需要通配符。

通配符用大括号括起来,写在文件名里。Snakemake 会自动匹配实际文件中的对应部分。

看这个例子:

rule clean_sample:
    input:
        "data/raw/{sample}.csv"
    output:
        "data/cleaned/{sample}.csv"
    shell:
        "python scripts/clean.py {input} {output}"

{sample} 就是通配符。它可以匹配任何字符序列。

当你执行 snakemake data/cleaned/01.csv 时,Snakemake 就知道 sample=01。

然后它自动寻找 data/raw/01.csv 作为输入。

你想想看,这就意味着你只需要一条规则,就能处理任意数量的样本。

2.4 多个通配符的组合

一个规则里可以有好几个通配符。

比如我要按日期和特征类型来处理:

rule extract_features:
    input:
        "data/cleaned/{date}/{feature_type}.csv"
    output:
        "data/features/{date}/{feature_type}.parquet"
    shell:
        "python scripts/extract.py {input} {output}"

当 Snakemake 需要生成 data/features/2024-01-01/rolling_min.parquet 时:

  • {date} → 2024-01-01
  • {feature_type} → rolling_min

它会自动寻找 data/cleaned/2024-01-01/rolling_min.csv 作为输入。

通配符模式 匹配示例 说明
{sample} sample_01、batch_A 匹配任意单段路径
{date} 2024-01-01、2024-02_batch 可包含连字符等字符
{feature} rolling_mean、max_5min 常用于特征名或类型
注意:通配符不能匹配目录分隔符 /。
如果你需要匹配多级目录,必须显式定义多个通配符,或者使用通配符与固定路径结合。

2.5 通配符的实际机制

通配符的解析,依赖于规则输出中的模式。

Snakemake 拿到目标文件路径后,会跟所有规则的 output 模式做匹配。

一旦匹配成功,通配符的值就被确定。然后 Snakemake 用这个值去填充 input 模式,得到真正的输入文件路径。

这个过程很聪明,但也有限制:

  • 一个规则里的所有通配符,值是一次确定的
  • 不同的规则可以有相同的通配符名,值可以不同
  • 输出文件中的通配符,必须在输入文件中出现,否则 Snakemake 不知道要依赖什么
我记得有一次,我在 output 里写了两个通配符,但 input 只用了其中一个。Snakemake 直接报错了。后来我明白了:output 里的通配符必须能在 input 中找到对应位置,不然它没法推导依赖关系。

2.6 规则命名与组织建议

规则名要清晰,我习惯用动词开头:clean_data、normalize_features、engineer_rolling_window。

这样一看就知道这个规则在干什么。

另外,不要把任何规则写得太长。一个规则最好只做一件事。

我早期写过一个大规则,里面包含清洗、归一化、特征提取三条命令。后来想重用其中一部分,才发现拆无可拆。

如果你有多个输出文件,可以用列表:

rule generate_features:
    output:
        "features/mean.csv",
        "features/std.csv",
        "features/max.csv"

这样一条规则可以同时产生多个成品。

Snakefile 规则与通配符核心流程 规则 (Rule) 输入 (input) 输出 (output) 动作 (shell/run) 规则A 输出 → 规则B 输入 规则B 输出 → 规则C 输入 规则C 输出 → 目标文件 通配符 {sample} 注入 匹配实际文件名 确定依赖路径 关键原则 output 中的通配符必须在 input 中出现,否则 Snakemake 无法推导上游依赖。 一条规则定义通配符 → 所有调用此规则的场合自动复用,无需重复编写。 基于规则的输入输出关系,结合通配符实现自动化流水线

2.7 避坑指南:通配符常见错误

我见过好几个新手在通配符上栽跟头。这里列几个最常见的:

  • 通配符不在输出中出现:如果输出写死了一个固定文件名,但输入用了通配符,Snakemake 无法解析,直接报错。
  • 多个通配符命名冲突:同一个规则里不能用两个同名通配符,但不同规则可以各自定义自己的同名通配符,互不干扰。
  • 路径不匹配:比如输出是 data/{sample}/result.csv,但目录 data/{sample} 不存在,Snakemake 也不会自动创建目录。需要手动处理目录结构。
我曾经写过一个规则,output 里是 data/{sample}/features.parquet,但 input 里用了 data/raw/{sample}.csv。执行时 Snakemake 报错说通配符 {sample} 在 input 中不完整。后来我把 input 改成 data/raw/{sample}/clean.csv,问题才解决。通配符的层级必须一致。

2.8 小结

规则的构成很简单:input、output、shell(或 run)。

输入输出是流水线的粘合剂,通配符是解决重复问题的利器。

记住:Snakemake 是靠文件路径来管理依赖的。只要你把输入输出关系写清楚,它就能自动搞定执行顺序和重跑判断。