4. Snakefile 文件结构:全局参数、规则定义、包含文件
Snakefile 是 Snakemake 的核心。说白了,它就是一个 Python 文件,只不过加了一些特殊的语法糖。但你千万不要把它当成普通 Python 脚本来写。一个好的 Snakefile 结构,能让你的流程清晰、可维护,团队协作起来也顺畅。
我个人习惯把 Snakefile 拆成三大块:全局参数、规则定义、包含文件。这三块各司其职,组合起来就是一套完整的工作流。为什么要这么区分呢?我来一个个讲清楚。
4.1 全局参数:流程的“总开关”
全局参数放在 Snakefile 的最顶部。我建议把 configfile、samples、output 等变量集中在这里。这样做的好处是,你一眼就能看到整个流程的输入输出配置,不用翻遍几百行代码去找一个路径变量。
举个例子,这是我在一个 RNA-seq 项目里用的全局参数区域:
configfile: "config.yaml"
# 读取样本信息
samples = pd.read_table(config["sample_table"])
sample_names = list(samples["sample_id"])
# 定义输出根目录
OUTDIR = config["output_dir"]
REPORTS = f"{OUTDIR}/reports"
LOGS = f"{OUTDIR}/logs"
# 定义软件路径(方便切换版本)
FASTQC = config["fastqc_path"]
TRIM = config["trimmomatic_path"]
你看,我把 configfile 放在第一行,然后读取样本表,最后定义几个全局路径变量。为什么这么搞?因为如果后期要改样本或路径,我只需要改 config.yaml,Snakefile 本身不动就好了。
OUTDIR、REPORTS,能让你在规则里一眼分出哪些是来自全局的固定值,哪些是局部变量。我在项目里一直这么用,团队 review 时也轻松不少。
4.2 规则定义:流程的“原子任务”
规则是 Snakefile 里最核心的部分。每条规则就是一个独立的任务,比如质控、比对、定量。我刚开始写 Snakefile 时,总想把所有事情塞进一条规则里——嗯,结果就是又慢又难调试。
后来我学乖了:一条规则只做一件事。比如这样:
rule fastqc:
input:
"{sample}_R1.fastq.gz"
output:
"fastqc/{sample}_fastqc.html"
log:
"logs/fastqc/{sample}.log"
params:
threads = 4
run:
shell("fastqc -t {params.threads} {input} -o fastqc/ > {log} 2>&1")
rule trimmomatic:
input:
r1 = "{sample}_R1.fastq.gz",
r2 = "{sample}_R2.fastq.gz"
output:
r1_trim = "trimmed/{sample}_R1_trim.fastq.gz",
r2_trim = "trimmed/{sample}_R2_trim.fastq.gz"
log:
"logs/trimmomatic/{sample}.log"
params:
adapters = "adapters/TruSeq3-PE.fa"
run:
shell("trimmomatic PE -threads 4 {input.r1} {input.r2} "
"{output.r1_trim} {output.r2_trim} "
"ILLUMINACLIP:{params.adapters}:2:30:10 > {log} 2>&1")
这里我有几点想提醒你:
- input 和 output 要用通配符
{sample},这样一条规则能处理所有样本。 - log 文件我建议都写进去,别偷懒。有一次我在项目里没写 log,跑了一夜结果报错,根本不知道哪一步出的问题,气得我肝疼。
- params 可以用来传递参数,比如线程数、参考基因组路径。这样规则内部运行
shell()时更清晰。
4.3 包含文件:给 Snakefile 瘦身
项目一大了,Snakefile 会变得臃肿。我见过有人把两百条规则堆在一个文件里,打开滚动条都拖半天。这种时候就要用 include 来分割了。
包含文件的思想很简单:把功能相近的规则抽到单独的文件里,然后在主 Snakefile 里用 include 引入。比如:
# 主 Snakefile
configfile: "config.yaml"
include: "rules/fastqc.smk"
include: "rules/trimming.smk"
include: "rules/alignment.smk"
include: "rules/quantification.smk"
# 最终的汇总规则
rule all:
input:
expand("reports/{sample}.qc_complete", sample=sample_names)
你看,这样主 Snakefile 就只剩几行 include 和一条 rule all 了。每个 .smk 文件里只放对应模块的规则,结构和调试都方便多了。
包含文件的写法跟 Snakefile 一样,只是文件后缀用 .smk 以示区别。比如 rules/fastqc.smk:
# rules/fastqc.smk
rule fastqc:
input:
"{sample}_R1.fastq.gz"
output:
"fastqc/{sample}_fastqc.html"
shell:
"fastqc -t 4 {input} -o fastqc/"
4.4 推荐目录与文件结构
说了这么多,给你一个我常用的项目结构作为参考:
| 文件/目录 | 作用 |
|---|---|
Snakefile |
主控文件,包含全局参数和 include 语句 |
config.yaml |
配置文件,存放样本表路径、软件路径、参数 |
rules/ |
存放所有 .smk 规则文件 |
data/ |
原始数据,按样本或批次存放 |
results/ |
分析结果输出目录 |
logs/ |
运行日志,对应每条规则的 log 路径 |
你可以直接把这个结构用到项目中,减少自己设计的时间。记得把 config.yaml 写得详细点,比如:
# config.yaml
sample_table: "data/samples.csv"
output_dir: "results"
fastqc_path: "/usr/bin/fastqc"
trimmomatic_path: "/usr/bin/trimmomatic"
star_path: "/opt/STAR/bin/Linux_x86_64/STAR"
threads: 8
memory: 32
/home/user/data/,结果换服务器跑全炸了。现在我只用 config.yaml 里的相对路径,移植到哪儿都行。
4.5 一点收尾思考
全局参数、规则定义、包含文件这三块,看似简单,但用好了能让你的 Snakemake 流程从“能跑”变成“优雅地跑”。我个人最享受的就是把所有逻辑拆到单独的 smk 文件里,看着主 Snakefile 干干净净的,心里就舒服。你试试看,绝对上瘾。