使用 Org Babel 编写计算笔记本
Org Babel 将 Emacs Org Mode 转变为一个交互式的、多语言的计算笔记本环境。与 Jupyter 或 Mathematica 类似,Org Babel 将可执行代码、实时执行结果、图表、表格和叙述性散文交织在一起。与基于 JSON 的笔记本不同,Org 笔记本是纯文本、人类可读、原生支持 Git 版本控制,并且支持在单个文档中混合使用多种编程语言。
1. 核心架构与关注点分离
每个 Org Babel 笔记本都围绕五个由标头参数控制的独特关注点运行。混淆它们是 Org 笔记本中产生 bug 的主要原因:
| 关注点 | 关键标头参数 | 控制内容 |
|---|---|---|
| 执行 | :session, :eval, :dir, :var, :cache | 块是否运行、在哪个进程中运行、在何处运行以及使用什么输入? |
| 结果 | :results (value vs output, 格式, 类型, 处理) | 从执行中捕获了什么以及如何在缓冲区中呈现? |
| 导出 | :exports (code, results, both, none) | 导出的报告(HTML, PDF, Markdown)中出现什么? |
| 缠结 | :tangle, :shebang, :noweb | 提取到磁盘上独立的源代码文件中的内容是什么? |
| 组合 | :var, #+CALL:, <<noweb>>, org-sbe | 块如何相互传递数据或代码? |
2. 笔记本结构与前导设置
一个 Org 笔记本文件 (.org) 从文档元数据和缓冲区范围的默认设置开始,然后是包含叙述性文本、数学公式和代码块的层级标题。
2.1 标准笔记本前导模板
#+TITLE: 实验 X 的计算分析
#+AUTHOR: 张三
#+DATE: [2025-05-18 Sun]
#+OPTIONS: toc:2 num:t ^:{}
#+STARTUP: overview hideblocks inlineimages
# 此缓冲区中所有块的全局 Babel 默认值:
#+PROPERTY: header-args :eval yes :exports both :results drawer
# 语言特定的默认值:
#+PROPERTY: header-args:python :session *py-analysis* :results value :exports both
#+PROPERTY: header-args:R :session *R-analysis* :results value :exports both
#+PROPERTY: header-args:sh :session none :results output :exports both
关键缓冲区内设置:
#+STARTUP: inlineimages:插入结果或打开缓冲区时,自动内联显示生成的图表/图像。#+STARTUP: hideblocks:启动时折叠代码块,仅显示叙述和结果以提高可读性。#+STARTUP: nohideblocks:显示所有展开的代码块。#+PROPERTY: header-args ...:缓冲区范围的默认值。可以使用header-args+进行追加。
2.2 在 Emacs 配置中启用语言
默认情况下,Org 仅启用 Emacs Lisp 求值。所有其他语言必须在你的 Emacs 配置中显式加载:
(org-babel-do-load-languages
'org-babel-load-languages
'((emacs-lisp . t)
(python . t)
(R . t)
(shell . t)
(sqlite . t)
(gnuplot . t)
(C . t)))
为了对受信任的笔记本避免交互式执行确认对话框:
;; nil 允许块在无需确认提示的情况下运行
(setq org-confirm-babel-evaluate nil)
3. 代码块结构
#+NAME: <block_name>
#+HEADER: <additional_header_args>
#+BEGIN_SRC <language> <switches> <header_arguments>
<body>
#+END_SRC
#+NAME: <name>:可选的唯一标识符。如果该块将被引用为函数、通过:var传递给另一个块作为输入、或通过#+CALL:调用,则此标识符是必需的。<language>:Babel 识别的标识符 (python,R,sh,emacs-lisp,sqlite,sql,julia,calc,gnuplot等)。<switches>:可选开关(例如,-n用于导出时行号,-r用于去掉标签)。<header_arguments>:控制执行、结果和导出的键值对。#+HEADER::用于清晰指定长列表标头参数的辅助行。
内联代码块
用于在段落中嵌入动态计算值:
最终计算出的准确率为 src_python[:session *py* :results value]{f"{acc * 100:.2f}%"}。
4. 标头参数参考
4.1 会话管理:交互式探索 vs 封闭式可重现性
:session none(默认):在一次性外部解释器进程中求值块。封闭、干净且避免状态污染,但每次运行都会重新初始化导入和环境。:session <name>:在持久的 Emacs 下级 REPL 缓冲区中求值(例如*py-analysis*)。- 变量、函数和导入的模块在共享同一会话名称的多个块之间持续存在。
- 对于大数据集、大模型权重和探索性工作流至关重要。
- 最佳实践: 使用
:session <name>进行原型开发;对于最终验证,请重新启动会话或使用:session none测试关键部分以保证可重现性。
4.2 工作目录与远程环境
:dir /path/to/project:在指定目录内运行块。:mkdirp yes:如果目标目录不存在,则创建它。- 远程执行 (TRAMP):
:dir /ssh:user@gpu-cluster:/workspace/data在服务器上远程执行块,同时将输出渲染到本地 Emacs 缓冲区中。
4.3 缓存昂贵的计算
:cache yes:计算块文本、输入和:var参数的 SHA1 哈希值。- 如果哈希值与存储在
#+RESULTS[hash]:行上的哈希值匹配,则跳过求值并保留缓存结果。 - 仅用于没有外部副作用的确定性纯计算。
5. 结果处理:关键区别
:results 参数最多包含四个正交组件:
:results [collection] [type] [format] [handling]
5.1 收集方式:函数式 (value) vs 脚本式 (output)
理解这种区别可以消除 90% 的 Babel 混淆:
| 收集模式 | 捕获的内容 | Python 行为 | Shell 行为 |
|---|---|---|---|
:results value (Python, R, elisp 的默认值) | 最后一个表达式的返回值或显式 return。 | 必须使用显式的 return <val> 语句。所有 print() 输出被丢弃。 | 返回退出码或字符串值。 |
:results output (shell, bash 的默认值) | 执行期间发出的 Stdout 流。 | 捕获所有 print() 语句。任何 return 都被忽略。 | 捕获所有命令的标准输出(echo, ls 等)。 |
Python 函数式示例 (:results value):
#+BEGIN_SRC python :results value
data = [1, 2, 3, 4]
# 在函数式模式下,Python 中需要显式 return:
return sum(data)
#+END_SRC
#+RESULTS:
: 10
Python 脚本式示例 (:results output):
#+BEGIN_SRC python :results output
for i in range(3):
print(f"Epoch {i}: loss={0.5/(i+1):.4f}")
#+END_SRC
#+RESULTS:
: Epoch 0: loss=0.5000
: Epoch 1: loss=0.2500
: Epoch 2: loss=0.1667
5.2 结果类型
table/vector:将返回的列表/数组格式化为 Org Mode 表格。scalar/verbatim:将输出格式化为原始引用文本,不进行表格格式化。file:将输出定向到磁盘上的文件(使用:file <filename>指定),并在结果中插入一个 Org 文件链接。
5.3 结果格式
drawer(推荐):将结果包裹在:RESULTS:…:END:分隔符中。保持文档整洁,特别适用于多行输出。raw:将输出直接插入为未转义的 Org 文本(用于生成动态标题或表格)。code:将输出包裹在#+BEGIN_SRC <lang>…#+END_SRC中。link/graphics:将结果视为图像/文件的路径并生成[[file:path]]。
5.4 处理方式
replace(默认):替换之前的#+RESULTS:块。append:在现有结果下方追加新结果。prepend:在现有结果上方插入新结果。silent:在 Emacs 回显区/迷你缓冲区显示结果,不写入 Org 缓冲区。
6. 笔记本中的可视化与图表
为了生成直接呈现在笔记本中的图表和图形:
6.1 Python Matplotlib 绘图
#+NAME: plot-normal-distribution
#+BEGIN_SRC python :session *py-vis* :results graphics file :file figures/normal_dist.png :exports both
import matplotlib.pyplot as plt
import numpy as np
plt.figure(figsize=(6, 3.5))
x = np.linspace(-3, 3, 200)
plt.plot(x, np.exp(-x**2 / 2) / np.sqrt(2 * np.pi), color="#2b5c8f", lw=2)
plt.title("标准正态分布")
plt.xlabel("z")
plt.ylabel("密度")
plt.tight_layout()
plt.savefig("figures/normal_dist.png", dpi=150)
plt.close()
#+END_SRC
#+RESULTS: plot-normal-distribution
[[file:figures/normal_dist.png]]
6.2 R ggplot2 绘图
#+NAME: r-scatter-plot
#+BEGIN_SRC R :session *r-vis* :results graphics file :file figures/r_scatter.png :exports both
library(ggplot2)
p <- ggplot(mtcars, aes(x=wt, y=mpg, color=factor(cyl))) +
geom_point(size=3) +
theme_minimal() +
labs(title="燃油效率与重量", x="重量 (1000 lbs)", y="MPG")
ggsave("figures/r_scatter.png", plot=p, width=6, height=4, dpi=150)
#+END_SRC
#+RESULTS: r-scatter-plot
[[file:figures/r_scatter.png]]
提示: 在 Emacs 中运行 M-x org-toggle-inline-images (C-c C-x C-v) 以切换在缓冲区中显示生成的图形。
7. 交互式执行与编辑工作流
基本快捷键
| 快捷键 | 命令 | 描述 |
|---|---|---|
C-c C-c | org-babel-execute-src-block | 执行光标下的块并插入/更新结果。 |
C-c ' | org-edit-special | 在专用的主模式缓冲区中打开块体(支持完整 LSP、语法高亮、自动补全)。再次按 C-c ' 保存并返回。 |
C-c C-v b | org-babel-execute-buffer | 顺序执行笔记本中的所有代码块(如“运行所有单元格”)。 |
C-c C-v s | org-babel-execute-subtree | 执行当前标题下的所有代码块。 |
C-c C-o | org-open-at-point | 在 #+RESULTS: 链接上时,在外部查看器或缓冲区中打开图像或生成的文件。 |
M-<down> | org-babel-pop-to-session | 将焦点直接切换到该块会话的交互式 REPL 缓冲区。 |
C-c C-v v | org-babel-expand-src-block | 预览完全展开的块(包含所有已解析的 :var 和 noweb 扩展)。 |
8. 导出与发布报告
一个笔记本可以编织成独立的 HTML、PDF 或 Markdown 文档。
8.1 使用 :exports 控制导出内容
:exports code:仅导出代码块。:exports results:仅导出输出/图形(隐藏代码,适用于高管报告)。:exports both:导出代码和输出(适用于技术文档)。:exports none:从导出的文档中隐藏代码和结果(如果需要,在求值期间运行)。
8.2 使用 :eval 控制导出求值
:eval never-export:防止在导出期间重新运行块,使用缓冲区中现有的#+RESULTS:。加快导出速度并防止非确定性更改。:eval query-export:在导出期间执行前提示。
8.3 导出命令
C-c C-e h h(org-html-export-to-html):生成 HTML 报告。C-c C-e h o(org-html-export-to-html-and-open):生成 HTML 并在浏览器中打开。C-c C-e l p(org-latex-export-to-pdf):通过 LaTeX 生成排版的 PDF 报告。
9. 常见陷阱与故障排查清单
| 症状 / 错误 | 原因 | 修复 |
|---|---|---|
| Python 块不返回结果 | 使用了 :results value 但使用了 print(...) 而不是 return ... | 将 print(...) 替换为 return ...,或将标头改为 :results output。 |
No org-babel-execute function for <lang> | 语言未在 org-babel-load-languages 中注册 | 在 Emacs 配置中向 org-babel-load-languages 添加 (<lang> . t) 并求值。 |
| 生成了图像链接但图像未显示 | Org Mode 中内联图像被关闭 | 运行 M-x org-toggle-inline-images (C-c C-x C-v) 或添加 #+STARTUP: inlineimages。 |
会话输出包含重复提示 (>>>) | REPL 缓冲区中存在残留的交互式提示文本 | 清理或重新启动会话缓冲区 (*session-name*)。 |
| 导出慢或挂起 | Org 在导出期间重新执行所有笔记本块 | 对长时间运行的块设置 :eval never-export,或通过 #+PROPERTY: header-args :eval never-export 全局设置。 |
| 表格输入产生水平规则错误 | 水平规则 (` | -`) 被视为原始行 |
10. 完整注释笔记本示例
#+TITLE: 太阳耀斑频率分析
#+AUTHOR: 亚历克斯·里维拉博士
#+DATE: [2025-05-18 Sun]
#+OPTIONS: toc:nil num:nil
#+STARTUP: inlineimages nohideblocks
#+PROPERTY: header-args :session *solar* :results drawer :exports both
* 简介
我们分析在 6 个月的观测窗口内记录的观测耀斑频率。
* 原始观测
以下是基线测量表:
#+NAME: raw_observations
| 月份 | C 级 | M 级 | X 级 |
|-------+---------+---------+---------|
| 一月 | 12 | 3 | 0 |
| 二月 | 19 | 5 | 1 |
| 三月 | 15 | 2 | 0 |
| 四月 | 27 | 8 | 2 |
| 五月 | 31 | 11 | 3 |
| 六月 | 22 | 7 | 1 |
* 数据处理与汇总统计
我们使用 Python 计算总太阳活动:
#+NAME: compute_totals
#+BEGIN_SRC python :var obs=raw_observations :colnames yes :results value table
total_events = []
for row in obs:
month = row[0]
total = sum(row[1:])
total_events.append([month, total])
return [["月份", "总事件数"]] + total_events
#+END_SRC
#+RESULTS: compute_totals
:RESULTS:
| 月份 | 总事件数 |
|-------+-----------|
| 一月 | 15 |
| 二月 | 25 |
| 三月 | 17 |
| 四月 | 37 |
| 五月 | 45 |
| 六月 | 30 |
:END: