使用 Org Babel 編寫計算筆記本
Org Babel 將 Emacs Org Mode 轉變為一個互動式的多語言計算筆記本環境。與 Jupyter 或 Mathematica 類似,Org Babel 交織了可執行代碼、即時執行結果、圖形、表格和敘述性文字。不同於基於 JSON 的筆記本,Org 筆記本是純文字、人類可讀、原生支援 Git 版本控制,並且允許在單一文件中混合使用多種程式語言。
1. 核心架構與關注點分離
每個 Org Babel 筆記本都透過標頭參數在五個不同的關注點上運作。混淆這些是 Org 筆記本中錯誤的主要來源:
| 關注點 | 關鍵標頭參數 | 控制內容 |
|---|---|---|
| 執行 | :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: Computational Analysis of Experiment X
#+AUTHOR: Jane Doe
#+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::用於整潔地指定長列表標頭參數的輔助行。
內聯程式碼區塊
為了在段落內嵌入動態計算值:
The final computed accuracy was src_python[:session *py* :results value]{f"{acc * 100:.2f}%"}.
4. 標頭參數參考
4.1 會話管理:互動式探索 vs 封閉式可重複性
:session none(預設):在一次性外部解釋器進程中評估區塊。封閉、乾淨且避免狀態污染,但每次運行時都會重新初始化導入和環境。:session <name>:在持久的 Emacs inferior 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 的預設值) | 執行期間發出的 標準輸出流。 | 捕獲所有 print() 語句。任何 return 都會被忽略。 | 捕獲所有命令的標準輸出(echo, ls 等)。 |
Python 函數式範例 (:results value):
#+BEGIN_SRC python :results value
data = [1, 2, 3, 4]
# 在函數模式下,Python 中需要顯式回傳:
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("Standard Normal Distribution")
plt.xlabel("z")
plt.ylabel("Density")
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="Fuel Efficiency vs Weight", x="Weight (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 設定中將 (<lang> . t) 添加到 org-babel-load-languages 並評估它。 |
| 產生了圖片連結但圖片未顯示 | 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: Solar Flare Frequency Analysis
#+AUTHOR: Dr. Alex Rivera
#+DATE: [2025-05-18 Sun]
#+OPTIONS: toc:nil num:nil
#+STARTUP: inlineimages nohideblocks
#+PROPERTY: header-args :session *solar* :results drawer :exports both
* Introduction
我們分析了在一個 6 個月的觀測窗口內記錄的觀測耀斑頻率。
* Raw Observations
這是基線測量表格:
#+NAME: raw_observations
| Month | C-Class | M-Class | X-Class |
|-------+---------+---------+---------|
| Jan | 12 | 3 | 0 |
| Feb | 19 | 5 | 1 |
| Mar | 15 | 2 | 0 |
| Apr | 27 | 8 | 2 |
| May | 31 | 11 | 3 |
| Jun | 22 | 7 | 1 |
* Data Processing & Summary Statistics
我們使用 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 [["Month", "Total Events"]] + total_events
#+END_SRC
#+RESULTS: compute_totals
:RESULTS:
| Month | Total Events |
|-------+--------------|
| Jan | 15 |
| Feb | 25 |
| Mar | 17 |
| Apr | 37 |
| May | 45 |
| Jun | 30 |
:END:
* Visualization
每月總事件數的長條圖:
#+NAME: flare_bar_chart
#+BEGIN_SRC python :session *solar* :results graphics file :file figures/flare_totals.png :exports both
import matplotlib.pyplot as plt
import numpy as np
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
totals = [15, 25, 17, 37, 45, 30]
plt.figure(figsize=(6, 4))
plt.bar(months, totals, color="#2b5c8f")
plt.title("Total Solar Flare Events by Month")
plt.xlabel("Month")
plt.ylabel("Total Events")
plt.tight_layout()
plt.savefig("figures/flare_totals.png", dpi=150)
plt.close()
#+END_SRC
#+RESULTS: flare_bar_chart
[[file:figures/flare_totals.png]]
* Conclusion
數據顯示在所觀測的期間內,太陽活動從 1 月到 5 月呈上升趨勢,並在 6 月略有下降。