跳到正文

Jupyter Book 生成文档站点

Jupyter Book 处理的是分析做完之后的交付环节。单篇 Notebook 适合自己反复调试,但一门课的实验指导、一份课题的完整分析记录,需要目录、章节顺序和站内搜索。Jupyter Book 把散落的 .ipynb 与 .md 组织成一个可发布、可搜索的静态站点。

R 语言用 Quarto 做同一件事,见 /r/reporting/quarto/。两者的共同点是把「写文档」和「跑代码」放进同一份源文件;区别在输入的形态,Jupyter Book 以 Notebook 为中心,Quarto 以文本为中心。

Jupyter Book 在 2.x 换过一次内核,这一步不能跳过。1.x 基于 Sphinx,配置文件是 _config.yml_toc.yml;2.x 改为基于 MyST 文档引擎(mystmd),配置文件合并成一个 myst.yml。两套命令与配置互不通用,网上多数教程仍是 1.x 的写法。

终端窗口
jupyter-book --version

2.x 只输出一行:

v2.1.6

1.x 输出一组依赖版本:

Jupyter Book : 1.0.4
External ToC : 1.1.0
MyST-Parser : 3.0.1
MyST-NB : 1.4.0
Sphinx Book Theme : 1.3.0

看到第二种,说明装的是 1.x。下文以 2.x 为准,1.x 的差异集中在「从 1.x 迁移」一节。

终端窗口
pip install jupyter-book

2.x 在构建时会检查 Node.js,版本不够会直接报错退出:

- node 22.23.2 Required: >= 18.0.0
- npm 10.9.8 Required: >=8.6.0
- myst 1.10.1

在项目目录里初始化:

终端窗口
jupyter-book init

不带参数时该命令会交互式提问,最后写出 myst.yml。要跳过提问,用显式开关:

终端窗口
jupyter-book init --project --site --write-toc

--project 生成项目段,--site 生成站点段,--write-toc 扫描目录下的 .md 与 .ipynb 并自动填好目录。另有 --gh-pages--gh-curvenote,用于顺带生成对应的部署工作流。

init 生成的骨架(去掉注释):

version: 1
project:
id: 1c21d7a8-7610-4a58-8b75-d9dd75bb164f
toc:
- file: index.md
- file: analysis.md
site:
template: book-theme

version: 1 是配置格式的版本号,与项目版本无关。project 段管内容:titledescriptionauthorsgithubtocsite 段管外观:template 选主题,options 下可放 faviconlogo

作者字段有一处容易踩的地方,中文姓名会触发警告:

project:
authors:
- name: 张三
myst.yml 'authors.0' No given name for name '张三' - if this is intended, you may define 'name' explicitly as an object

解析器按空格拆分姓名,拆不出姓与名就告警。把 name 写成包含 givenfamily 的对象即可消除:

project:
authors:
- name:
given:
family:

givenfamily 必须是 name 的子键。直接与 name 平级会被判定为多余键并忽略,警告照旧。

--write-toc 按文件名字母序生成列表,正式写书时需要手工调整顺序与层级:

project:
toc:
- file: index.md
- title: 数据准备
children:
- file: cleaning.md
- file: merging.md
- title: 建模
children:
- file: regression.md
- file: nb.ipynb

file 的路径不带扩展名之外的前缀,相对于 myst.yml 所在目录。.md.ipynb 可以混排在同一层,构建时一视同仁,Notebook 的单元格输出会直接渲染进页面。分组用 titlechildren,嵌套层数不设上限,侧边栏的折叠层级与这里一一对应。

index.md 必须放在最外层的第一项,它对应站点的首页;放进 children 里站点就没有入口页。

Jupyter Book 2 的正文由 MyST 解析。它是 Markdown 的超集,Markdown 的写法全部有效,额外提供三类结构。

指令(directive),用三个冒号包裹,用于插入块级元素:

:::{note}
样本量小于 30 时,正态性检验的结论不稳定。
:::

角色(role),用花括号加冒号,用于行内标记,如 {math}{term}

交叉引用,给标题或图表一个标签,正文里用 # 引用:

(my-section)=
## 数据准备
见 [](#my-section)

标签行必须紧贴在标题上方,中间不能有空行。渲染后 [](#my-section) 自动变成章节编号,插入新章节时后续编号自动重排。

图表用 figure 指令,namefig- 开头才能被引用:

```{figure} logo.png
:name: fig-logo
:width: 200px
站点标志。
```

图片路径写错时构建不中断,只在终端报一行提示,页面上的图留空:

fig.md Cannot find image "logo.png" in /tmp/jbtest

本篇引用的构建日志均为日志正文,终端里每行前面还会多一个图标前缀,此处为排版省略。

默认情况下 build 不执行 Notebook,它只读取 .ipynb 里已有的一组输出。想让构建过程重跑代码,加 --execute

终端窗口
jupyter-book build --execute

终端会显示每个被执行的 notebook 以及缓存命中情况:

Executing notebook (nb.ipynb) [no execution cache found]

执行需要本机有可用的 Jupyter 内核。内核缺失时报错如下,构建继续,但页面用的是旧输出:

nb.ipynb Could not load Jupyter session manager to run executable nodes

多个 notebook 并发执行,--execute-parallel 控制并发数,默认 3。并发数调高会同时占用更多内存,跑大模型或大数据集时按机器配置下调。

正文里要插入可执行代码块时用 {code-cell},块选项写在块内,以冒号开头:

```{code-cell} python
:tags: [hide-input]
import numpy as np
x = np.array([4.2, 11.5, 7.3, 5.8, 6.4, 10.0])
print(f"均值 {x.mean():.2f}")
```

:tags: [hide-input] 让代码折叠、只留输出,写教程时常用。这与 Notebook 单元格自带的 tags 是同一套机制,两种源文件里可以写同样的标签。

终端窗口
jupyter-book build --site # 生成站点内容
jupyter-book build --pdf # 导出 PDF
jupyter-book build --word # 导出 Word
jupyter-book build --all # 全部格式

--site 是网页,其余格式各自独立。构建成功的终端输出会列出每个源文件与总页数:

Built index.md in 19 ms.
Built analysis.md in 18 ms.
Built 2 pages for project in 47 ms.

站点内容与主题是两件事。--site 先把 MyST 源文件编译成内容,再从远端拉取 site.template 指定的主题包:

Querying template metadata from https://api.mystmd.org/templates/site/myst/book-theme
Fetching template from https://github.com/myst-templates/book-theme/archive/refs/heads/main.zip

主题包走 GitHub,网络不通时这一步失败,但前面的内容编译已经完成,输出里能看到 Built N pages。这意味着内网机器可以把主题包预先缓存到本地,或改用 --html 只产出静态页面内容。

导出的产物在 _build 目录,用 jupyter-book clean 清理。该命令只删导出物与临时文件,不动源文件。

初始化时带上 --gh-pages,会在 .github/workflows/ 下生成一个工作流文件,推送到 main 分支后由 Actions 构建并发布。手工部署走同一套思路:构建站点,再把产物推到 gh-pages 分支。

仓库信息写进 project.github 后,构建出的页面会带上「在 GitHub 上编辑此页」的链接,读者发现笔误可以直接提 PR。这一点与 Quarto 的 repo-url 是同一个用途。

1.x 与 2.x 的差别不是配置项改名,是整条工具链换掉了。对应关系如下:

1.x 2.x
_config.yml + _toc.yml myst.yml 单文件
jupyter-book create PATH jupyter-book init
jupyter-book build PATH jupyter-book build
format: jb-book + root: + chapters: project.toc 的嵌套列表
依赖 Sphinx 与 sphinx-book-theme 依赖 Node.js 与 mystmd

1.x 的目录文件长这样,root 指定首页,chapters 是平铺列表:

format: jb-book
root: intro
chapters:
- file: markdown
- file: notebooks

1.x 的 _config.yml 里,execute.execute_notebooks 控制是否重跑;2.x 改成命令行开关 --execute,默认不执行。迁移时最需要留意这一处,1.x 默认 force(每次都跑),2.x 默认不跑,行为相反。

已经写好的 Markdown 与 Notebook 内容不必改动,MyST 的指令与角色语法在两版之间兼容。真正要重写的是配置文件和构建命令,通常半小时之内能完成。

  • 路径写错是最常见的失败原因。toc 里的 file 相对于 myst.yml,图片路径相对于当前源文件,两套基准不一样。
  • 忘记 --execute,页面显示的是上次运行的结果。Notebook 里的输出不会自动更新。
  • 构建缓存。--execute 命中缓存时不会重跑,改了数据源要先 jupyter-book clean
  • 中文 PDF 需要 LaTeX 环境与中文字体,--pdf 在没装 TeX 的机器上直接失败。想省事就先出网页,需要投稿时再单独处理 PDF。
  • 主题包要联网拉取,离线环境必须提前缓存,否则内容编译成功而站点无法生成。

站点建好之后,把环境与随机种子固定下来才能让结果可复现,见 /python/reporting/reproducible/。如果只是想交付单篇分析给导师看,用 nbconvert 导出 HTML 就够了,见 /python/reporting/notebook-export/