跳到正文

Jupyter Notebook 导出:HTML、PDF 与幻灯片

Notebook 适合探索,不适合交付。.ipynb 拿到别人手里,对方得先装 Jupyter、再对齐包版本才能打开,而文件里混着代码、输出和一份可能已经过期的执行状态。把它在命令行里转换成一份自包含的 HTML 或 PDF,分析才算真正交出去。

转换工具是 nbconvert,它是 Jupyter 的一个独立包,pip install jupyterlab 时已经一并装上。命令行入口有两个写法,效果相同:

终端窗口
jupyter nbconvert --to html report.ipynb
python -m jupyter nbconvert --to html report.ipynb

第二种写法把解释器钉死在当前环境上,和 /python/basics/jupyter/ 里讲内核错位问题时给的建议是同一个道理。

--to 后面接格式名,最常用的五种:

格式 产物 适用场景
html 单个 .html 发给同门、贴进论文附录、做预注册材料
markdown .md 加图片文件夹 需要继续用 Pandoc 加工,或导入其他文档系统
python .py 脚本 把探索固化成可重跑的脚本
slides .slides.html 组会汇报,基于 Reveal.js
pdf .pdf 打印或提交,依赖最重

导出一份 HTML 的实际输出是两行:

终端窗口
$ jupyter nbconvert --to html report.ipynb
[NbConvertApp] Converting notebook report.ipynb to html
[NbConvertApp] Writing 279866 bytes to report.html

HTML 文件偏大是正常现象。上面这 279866 个字符里,有 262988 个落在内嵌的 <style> 块中——样式表和图标字体占了 94%,报告本身的内容只有几 KB。换来的是单文件自包含,双击就能打开,不依赖网络也不依赖对方的 Python 环境。

--to markdown 有一个容易忽略的细节:转换出来的代码块不带语言标注

```
import pandas as pd
agg = pd.DataFrame({
'supp': ['OJ', 'VC', 'OJ', 'VC', 'OJ', 'VC'],
...
})
```

围栏后面是空的,没有 python 三个字。把这份 Markdown 交给静态站点生成器,代码不会高亮。需要高亮就得自己补标注,或者干脆直接从别处复制代码。

--to python 保留单元格的切分痕迹,但把 Markdown 单元格改写成注释:

#!/usr/bin/env python
# coding: utf-8
# # ToothGrowth 剂量效应分析
#
# 本报告统计不同剂量下的均值。
# In[ ]:
import pandas as pd

# In[ ]: 里的方括号是空的,因为导出脚本时并不执行代码。这个文件可以直接 python report.py 跑,它此时是一个普通脚本。

默认的 nbconvert 不执行代码,它只做格式转换。Notebook 里存的什么输出,转换出来就是什么输出。这意味着一个很久没重跑过的 notebook 会导出一份带着旧数字的 HTML,而且看不出任何异常。

让 nbconvert 先执行再转换,加 --execute

终端窗口
$ jupyter nbconvert --to html --execute report.ipynb
[NbConvertApp] Converting notebook report.ipynb to html
[NbConvertApp] Writing 281461 bytes to executed.html

报告的字符数比未执行时多出 1595,这一部分就是这次执行真正产生的输出。执行发生在转换之前,用的是 Notebook 自己的内核。

执行卡住是常见故障。默认的超时对大数据集或模型拟合来说偏短,报错信息里会点明是哪一个单元格超时:

终端窗口
jupyter nbconvert --to html --execute report.ipynb \
--ExecutePreprocessor.timeout=600

timeout 的单位是秒,设成 None 表示不限时,但不建议——一个死循环会让导出命令挂到天荒地老。另一个相关参数是 --allow-errors,它让某个单元格报错时继续转换,并把错误信息留在输出里。默认行为是遇到错误直接中止,这对例行报告是对的:宁可导出失败,也不要发出一份中间结果残缺的报告。

Notebook 里的输出经常是几次不同运行拼起来的,彼此不一致。先清空再重新执行,比直接转换可靠:

终端窗口
$ jupyter nbconvert --clear-output --inplace report.ipynb
[NbConvertApp] Converting notebook report.ipynb to notebook
[NbConvertApp] Writing 815 bytes to report.ipynb

注意这里的转换目标是 notebook 自己,--inplace 表示覆盖原文件。上面这份示例 notebook 本来就没有存过输出,所以数字变化不大;换成一份存了几千字符输出的 notebook,同一命令把它从 5004 字节压回 840 字节。这里有个容易看错的地方:nbconvert 打印的这个数字统计的是字符数,含中文的文件在磁盘上会比它大一些。清理命令在提交前跑一次,git diff 就干净了。

--clear-output 不带 --inplace 时把清理后的 notebook 写到新文件,原文件不动。

每种格式都有默认模板。查询与切换模板都用 --template

终端窗口
jupyter nbconvert --to html --template classic report.ipynb
jupyter nbconvert --to html --template lab report.ipynb

已安装的模板名可以从 nbconvert 的模板目录看到:

asciidoc base basic classic compatibility lab latex
markdown python reveal rst script skeleton webpdf

HTML 的默认模板是 lab,它复刻 JupyterLab 的界面配色;classic 是 Jupyter Notebook 7 之前的样式,看起来更像老版界面。HTML 导出还接受 --theme,默认值是 light,装了对应的预编译扩展时可以切成深色主题。

模板真正的作用是改结构,不只是换皮肤。要隐藏某些单元格、增删页眉页脚、嵌入机构模板,都得基于模板改。下一节是最常用的一类改动。

Notebook 里可以给单元格打标签(View → Cell Toolbar → Tags),常见的三个约定是 remove-cellhide-inputhide-output。nbconvert 默认不理会这些标签,它们只是一个记录。要让标签生效,需要一个配置文件:

nbconvert_config.py
c = get_config()
c.TagRemovePreprocessor.enabled = True
c.TagRemovePreprocessor.remove_cell_tags = {"remove-cell"}
c.TagRemovePreprocessor.remove_input_tags = {"hide-input"}

然后带上 --config

终端窗口
jupyter nbconvert --to html report.ipynb --config nbconvert_config.py

两个参数的区别值得分清。remove_cell_tags 把整个单元格删掉,连输出一起——适合放调试代码、内部备注、不想让导师看到的数据检查步骤。remove_input_tags 只删代码保留输出,图上还留着,产生它的那段代码不出现——报告正文里展示结论时用这个。

验证方式是看产物本身。给第一个 Markdown 单元格打上 remove-cell 再转换,导出的 Markdown 里那一节标题直接消失;给代码单元格打上 hide-input,同一份 Markdown 从 288 字节缩到 81 字节,少掉的正是那段代码块。

值得留意的是,标签在默认导出里仍然会以 CSS 类的形式留在 HTML 中,所以光在产物里搜 hide-input 字符串不能判断配置有没有生效,得直接看那一块内容在不在。

--to slides 生成的是 Reveal.js 页面:

终端窗口
$ jupyter nbconvert --to slides report.ipynb
[NbConvertApp] Converting notebook report.ipynb to slides
[NbConvertApp] Writing 271804 bytes to report.slides.html

分页规则由 Markdown 单元格决定。两种写法:单独一行写 ---,或者在标题前加 ### 让每级标题各起一页。第二种更省事,工作流是「一个 Markdown 标题 = 一张幻灯片」,讲稿的组织和代码的组织自然对齐。

Reveal.js 的主题通过 --SlidesExporter.reveal_theme 指定,可选值包括 simpleskybeigeserifsolarized 等;切换动画用 --SlidesExporter.reveal_transition。幻灯片默认从 CDN 加载 Reveal.js 资源,会议室断网就白屏,加 --SlidesExporter.reveal_url_prefix 或者用 --post serve 本地起服务都能规避。真正要离线演示,记得提前在目标机器上试一次。

PDF 是依赖最重的格式,有两条技术路线,都不轻松。

第一条走 LaTeX:--to pdf 先用 Pandoc 把 notebook 转成 LaTeX,再调用本机 LaTeX 引擎编译。缺 Pandoc 时的报错很直接:

nbconvert.utils.pandoc.PandocMissing: Pandoc wasn't found.
Please check that pandoc is installed:
https://pandoc.org/installing.html

装齐 Pandoc 和 LaTeX 之后还有中文这一关。pdflatex 处理中文会报字体错误,要换 XeLaTeX 并指定一个系统里确实存在的中文字体——思路与 R 语言 /r/reporting/rmarkdown-basics/CJKmainfont 那段完全一致。R 那边推荐 TinyTeX 作为轻量发行版,Python 这边可以装同一套,用 tinytex 的安装脚本或直接装 TeX Live。

第二条走浏览器:--to webpdf 用 Playwright 驱动无头 Chromium 把 HTML 打印成 PDF,完全绕开 LaTeX,中文天然正常。代价是要装浏览器内核。缺 Playwright 时报错同样明确:

RuntimeError: Playwright is not installed to support Web PDF conversion.
Please install `nbconvert[webpdf]` to enable.
终端窗口
pip install "nbconvert[webpdf]"
playwright install chromium

第二条命令的浏览器下载步骤未在本机实测,装完 Playwright 后按官方说明执行即可。

选择依据不复杂:报告里数学公式多、需要 LaTeX 的排版质量,走第一条;只是想把手上的 HTML 变成 PDF,走第二条。

转换多个文件时,nbconvert 接受通配符:

终端窗口
$ jupyter nbconvert --to html --output-dir out *.ipynb
[NbConvertApp] Making directory out
[NbConvertApp] Converting notebook nb2.ipynb to html
[NbConvertApp] Writing 279863 bytes to out/nb2.html
[NbConvertApp] Converting notebook report.ipynb to html
[NbConvertApp] Writing 279866 bytes to out/report.html

目录不存在时会自动创建。--output-dir 缺省是把产物写在每个 notebook 自己的目录旁边,把中间产物和源文件混在一起,不适合版本控制,建议每次都用它指定一个已经加进 .gitignore 的输出目录。

批量导出配合 --execute 就是一份例行报告流水线:数据更新后跑一条命令,所有报告重新执行、重新转换。

图片用相对路径引用时会失效。 Notebook 里 plt.savefig("fig1.png") 存到了工作目录,Markdown 或 LaTeX 导出后产物被移到别处,图片路径对不上,得到一堆裂图。解决办法是导出时统一加 --output-dir,把图片和文档放进同一个目录,或者改用 --to html 让图形以 base64 内嵌。

执行超时未必是代码慢。 --execute 用的是 Notebook 自己的内核,如果那个 kernel 指向的环境缺少包,报的是 ModuleNotFoundError 而不是超时,两者容易混。导出失败时先看完整报错,别急着调大 timeout

导出后中文乱码。 与 R 语言一样,先确认 .ipynb 存成 UTF-8。Notebook 文件是 JSON,正常情况下就是 UTF-8,但从 Windows 环境拷来的旧文件值得检查一次。

交互式组件导出后失效。 ipywidgets、plotly 的交互图在 HTML 里能保留一部分,但在 PDF 和 Markdown 里必然退化成静态内容。需要交互的部分单独发一份 HTML,正式文档里用静态图。

到这里 Python 语言的第一种交付方式就通了。需要整本书或一整套文档站点时,逐篇导出就不够用了,下一篇 /python/reporting/jupyter-book/ 讲怎么把多个 Notebook 组织成一个可导航的站点。