编写第一条规则:让 Snakemake 动起来

我习惯把 Snakemake 看作一个「听话的调度员」——你告诉它想要的最终文件是什么,它自己推导出该执行哪些步骤。听起来是不是有点像 Makefile?没错,Snakemake 的核心理念就是从 Make 那边继承来的,只不过用 Python 语法包装了一层。

这一章咱们就亲手写一条最简规则,跑通一个完整的工作流。目标很纯粹:输入一个原始文件,经过规则处理,输出一个新文件。等这一步跑通了,后面批量对接的复杂流程才有基础。

1. 核心概念:规则 = 输入 + 输出 + 动作

在 Snakemake 里,一切靠 规则(rule) 驱动。每条规则描述三件事:

  • input:规则依赖的文件(可以是多个)
  • output:规则生成的文件(也是告诉 Snakemake 哪些文件可以被其他规则引用)
  • 动作(shell / run / script):真正产生输出的命令或脚本

你看,这不就是三个核心要素嘛。我第一次理解 Snakemake 时,脑子里对应的是「原料→加工→成品」,一下子就好懂了。

Snakemake 规则与执行流程 input data/raw/input.txt 规则:process_data input + shell → output output data/processed/result.csv snakemake -j 1 Snakemake 根据 output 文件推导需要执行的规则,然后按依赖顺序运行

2. 实战:一个完整的 Snakefile

好,现在试着写一条能真正运行的规则。

2.1 准备目录与文件

先在项目根目录创建如下结构:

project/
├── Snakefile
├── data/
│   └── raw/
│       └── input.txt
└── scripts/
    └── convert.py

input.txt 的内容随便写几行文字,比如:

mol1
mol2
mol3

convert.py 是一个超简单的 Python 脚本,模拟对接处理前的数据转化:

import sys
input_file = sys.argv[1]
output_file = sys.argv[2]

with open(input_file) as f:
    lines = f.readlines()

with open(output_file, 'w') as f:
    f.write("name,status\n")
    for line in lines:
        name = line.strip()
        f.write(f"{name},processed\n")

2.2 编写 Snakefile

在项目根目录新建 Snakefile(注意大小写,Snakemake 默认找这个文件),写入:

rule process_data:
    input:
        "data/raw/input.txt"
    output:
        "data/processed/result.csv"
    shell:
        "python scripts/convert.py {input} {output}"

这里需要注意几点:

  • {input} 和 {output} 是 Snakemake 自动替换的特殊关键字,分别对应规则的 input 和 output 文件路径。
  • output 目录 data/processed/ 不用提前创建,Snakemake 会自动建(如果父目录不存在会报错,但 Python 的 open 不会自动建目录——这里提前建好最保险)。
  • 规则名 process_data 可以随便起,但建议语义化,方便后续 debug 时一眼看懂。

3. 运行与调试:-n 和 -j

Snakemake 提供了两个最常用的命令行参数:

参数用途常见的坑
-n 「干跑」(dry-run),只列出要执行哪些规则,不真正执行 shell 命令 有时日志不够详细,你可以加 -p 显示实际命令
-j N 指定并行任务数(N 为整数,-j 1 表示单线程) 如果规则之间有文件依赖,⚠️ 不要随意设大并行数,可能导致磁盘 I/O 竞争

3.1 先干跑,别急着执行

snakemake -n

终端会输出类似这样的信息:

Building DAG of jobs...
Job stats:
job            count    min threads    max threads
-----------  -------  -------------  -------------
process_data        1              1              1
total               1              1              1

[Tue Jan  9 10:00:00 2025]
rule process_data:
    input: data/raw/input.txt
    output: data/processed/result.csv
    jobid: 0
    reason: Missing output files: data/processed/result.csv
    resources: tmpdir=/tmp

这里最关键的一行是 reason,它告诉你为什么这个规则被触发——通常是因为 output 文件不存在。如果 output 已经存在且比 input 新,Snakemake 会跳过这条规则。我曾经因为这个特性踩过一次坑:调整了 input 文件却忘了重新跑,结果 Snakemake 以为 output 是最新的,导致对接了一堆旧数据。从那以后我每次修改完数据都会先 snakemake -n 确认一下。

3.2 正式运行

确认干跑无误后,用 -j 开始执行:

snakemake -j 1

输出会显示每条规则的 start 和 finish 时间,以及最终产物路径。

如果一切顺利,你会看到 data/processed/result.csv 被成功创建,内容类似:

name,status
mol1,processed
mol2,processed
mol3,processed

4. 常见问题与避坑指南

  • shell 命令写错路径:Snakemake 默认以 Snakefile 所在目录为工作目录。如果你的脚本放在子目录,一定要写相对路径(比如 scripts/convert.py),不要写成 ./scripts/convert.py 有时也会引起歧义,建议统一用相对路径不带 ./。
  • output 目录不存在:Snakemake 不会自动创建 output 的父目录(除非你在 shell 里用 mkdir -p)。我曾经有个项目,批量产生上千个输出文件,忘记建目录导致所有任务全挂。现在我习惯在规则里加一句 mkdir -p $(dirname {output})。
  • 文件锁冲突:如果多个 Snakemake 进程同时在同一个目录下运行,Snakemake 会尝试加锁(lock)。出现「Directory cannot be locked」时,说明有其他实例在运行,或者上次运行意外退出后锁没释放。手动删除 .snakemake/ 目录里的 lock 文件可解决,但记得先确认没有其他进程在写。

我的习惯:每次新建工作流时,我先写一条 all 规则作为入口,让 Snakemake 知道最终要什么文件。all 规则通常只有 input,没有 output 和 shell。这样后续添加其他规则时会特别顺手。不过这一章为了保持简单,咱们没加——等下一章讲到通配符时,我再详细介绍 all 规则。

调试小技巧:如果规则报了奇怪的错误,试试 snakemake -n -p。-p 会让 Snakemake 打印出实际要执行的 shell 命令内容,你可以在终端里手动跑一遍这个命令,看看到底是脚本逻辑问题还是路径问题。

千万别把 output 写成 input 相同的文件:Snakemake 不允许规则的 output 与 input 路径重叠(除非使用通配符+特殊配置),否则会触发循环依赖检测并报错。我刚开始写的时候犯过这种低级错误,调试了半天才发现是 output 写到了 input 同目录。

5. 小结:你已经会写第一条规则了

这一章没有涉及复杂的通配符和批量处理,但核心的规则三要素——input、output、shell——你已经完全掌握了。剩下的就是把规则的数量从「1 条」变成「多条+通配符」,这正是后续章节要展开的。

嗯,记得把今天写的 Snakefile 找个地方保存好,后面我们会基于它扩展成真正的批量对接流程。手动跑一遍试试看,见证 Snakemake 如何自动帮你管理文件和依赖,真的很有成就感。