- 开发工具
【免费下载链接】jupytext
Jupyter Notebooks as Markdown Documents, Julia, Python or R scripts
Jupytext 的核心能力之一,是把任意内核的 Jupyter Notebook 双向转换为可读的文本格式。本文以仓库中tests/data/notebooks/outputs/ipynb_to_md/ijavascript.md这份由 IJavascript(Node.js)内核 notebook 转换而来的 Markdown 镜像样例为线索,逐层拆解ipynb → md的转换规则:YAML 头部如何记录内核信息、Markdown 单元格与代码单元格如何映射为 Markdown 元素、输出内容为何被丢弃,并结合源码格式定义与镜像测试,说明这一转换链路的实现与稳定性保障。读完本文,你将掌握 Jupytext Markdown 格式(.md)的完整结构约定,并能在自己的 JavaScript notebook 上复现同样的转换。
一、样例全景:一份 IJavascript Notebook 的 Markdown 化身
关联文档tests/data/notebooks/outputs/ipynb_to_md/ijavascript.md是 Jupytext 测试体系中"镜像文件"(mirror file)的产物,即由同名输入 notebook 自动生成的固定参照文本。其完整内容如下:
--- jupyter: kernelspec: display_name: Javascript (Node.js) language: javascript name: javascript --- ## A notebook that uses IJavascript kernel ```javascript let x = 5; const y = 6; var z = 10;x + y;function add(num1, num2) { return num1 + num2 }add(x, y);const arrowAdd = (num1, num2) => num1 + num2;arrowAdd(x, y);const myCar = { color: "blue", weight: 850, model: "fiat", start: () => "car started!", doors: [1,2,3,4] }console.log("color:", myCar.color);console.log("start:", myCar.start());for (let door of myCar.doors) { console.log("I'm door", door) }myCar;class User { constructor(name){ this.name = name; } sayHello(){ return "Hello, I'm " + this.name; } }let John = new User("John"); John.sayHello();这份文本虽然只有几十行,却完整地体现了 Jupytext Markdown 格式的三大构成要素: 1. **YAML 前置元数据块**:由 `---` 包裹,记录 `jupyter.kernelspec`(内核显示名、语言与内核名),保证 Markdown 文档可以被还原为带相同内核声明的 notebook; 2. **Markdown 单元格**:直接以原样 Markdown 写入正文,如标题 `## A notebook that uses IJavascript kernel`; 3. **代码单元格**:统一以围栏代码块(fenced code block)` ```javascript ` 呈现,语言标识取自 notebook 的 `kernelspec.language`(此处为 `javascript`)。 值得注意的是:原 notebook 中代码单元格的执行输出(`stdout` 流与 `execute_result` 结果)在 Markdown 文本中一律不保留,这正是 Jupytext "代码与文档优先、输出交还 Jupyter" 的设计理念——文本格式聚焦于可版本化、可 diff 的源码与正文,输出则留在 `.ipynb` 中。 ## 二、输入对照:同一份 Notebook 的 ipynb 原始结构 要理解这份 Markdown 是如何生成的,需对照其输入:`tests/data/notebooks/inputs/ipynb_js/ijavascript.ipynb`。该 notebook 使用 IJavascript 内核,`nbformat` 为 4(`nbformat_minor` 为 2),`metadata.kernelspec` 声明如下: ```json "kernelspec": { "display_name": "Javascript (Node.js)", "language": "javascript", "name": "javascript" }, "language_info": { "file_extension": ".js", "mimetype": "application/javascript", "name": "javascript", "version": "11.14.0" }其 14 个单元格的结构与转换后文本的对应关系如下表:
| ipynb 单元格 | 类型 | 内容概要 | Markdown 中的形态 |
|---|---|---|---|
| 第 1 个 | markdown | 标题## A notebook that uses IJavascript kernel | 原样 Markdown 文本 |
| 第 2 个 | code | let/const/var变量声明 | ```javascript代码块 |
| 第 3 个 | code | x + y;(输出11) | 代码块,输出被丢弃 |
| 第 4 个 | code | function add(...)定义 | 代码块 |
| 第 5 个 | code | add(x, y);(输出11) | 代码块,输出被丢弃 |
| 第 6 个 | code | 箭头函数arrowAdd | 代码块 |
| 第 7 个 | code | arrowAdd(x, y);(输出11) | 代码块,输出被丢弃 |
| 第 8 个 | code | 对象字面量myCar | 代码块 |
| 第 9 个 | code | console.log("color:", ...)(stdout) | 代码块,输出被丢弃 |
| 第 10 个 | code | console.log("start:", ...)(stdout) | 代码块,输出被丢弃 |
| 第 11 个 | code | for...of遍历(4 行 stdout) | 代码块,输出被丢弃 |
| 第 12 个 | code | myCar;(execute_result 对象) | 代码块,输出被丢弃 |
| 第 13 个 | code | class User定义 | 代码块 |
| 第 14 个 | code | new User("John").sayHello()(输出"Hello, I'm John") | 代码块,输出被丢弃 |
可见转换是逐单元格、保序、无损的:Markdown 单元格原文保留,代码单元格逐字进入围栏代码块,唯一被剥离的是执行输出与execution_count。这正是 Jupytext 文本格式能做到"最小化变更"(minimal changes)的前提——输出不进入文本,源码的编辑不会因执行结果而产生 diff 噪音。
三、Markdown 格式的源码定义:MarkdownCellReader 与 MarkdownCellExporter
.md格式在 Jupytext 中并非临时拼凑,而是有正式注册的格式描述。在 src/jupytext/formats.py 中,markdown 格式被声明为:
NotebookFormatDescription( format_name="markdown", extension=".md", header_prefix="", cell_reader_class=MarkdownCellReader, cell_exporter_class=MarkdownCellExporter, # Version 1.0 on 2018-08-31 - jupytext v0.6.0 : Initial version # ... # Version 1.3 on 2021-01-24 - jupytext v1.10.0 : # Code cells may start with more than three backticks (#712) current_version_number="1.3", min_readable_version_number="1.0", ),该定义揭示了几个关键点:
- 格式版本:Markdown 格式当前为
1.3,最低可读版本1.0。自 2018 年 v0.6.0 诞生以来历经演进,1.3 版本起代码单元格可以以超过三个反引号开头(针对源码中本身含反引号的情况,见 issues #712),保证高版本产物可被低版本 Jupytext 读取; - 读写分工:读取由
MarkdownCellReader负责(把 Markdown 文本解析回 notebook 单元格),写出由MarkdownCellExporter负责(把 notebook 单元格序列化为上述文本),两者定义于 src/jupytext/cell_to_text.py; - 同族变体:
formats.py中还注册了扩展名为.markdown的同一格式(版本 1.2),以及同为 Markdown 家族但编码约定不同的 R Markdown(.Rmd,版本 1.2),说明 Jupytext 将 Markdown 系格式统一管理。
从源码结构看,MarkdownCellExporter的写出逻辑正是本文样例的生成者:它将 markdown 单元格直接写入正文行,将 code 单元格包裹在以语言名(如javascript)为标识的围栏代码块中,并在文件开头输出由 notebook 元数据(jupyter.kernelspec)生成的 YAML 头。而MarkdownCellReader的解析则是对称的逆过程,从而支持md → ipynb的反向还原。
四、语言映射:javascript 内核如何得到//注释与代码块标识
样例中所有代码块都以javascript作为围栏语言,这一标识直接来源于 notebook 的kernelspec.language。但 Jupytext 对语言的处理不止于此:在 src/jupytext/languages.py 中,javascript/js被登记为可识别语言,且脚本扩展名.js被映射为:
".js": {"language": "javascript", "comment": "//"},这条映射的意义在于:当同一份 notebook 被转换为脚本类格式(如 percent、hydrogen、light)时,.js文件将以//作为注释前缀来生成单元格分隔标记与元数据注释。换言之,Markdown 样例中"语言标识"与脚本样例中"注释风格"来自同一份语言注册表,构成了 Jupytext 多格式输出的一致基础。
仓库中的其他镜像目录(如 tests/data/notebooks/outputs/ipynb_to_percent/ijavascript.js、tests/data/notebooks/outputs/ipynb_to_hydrogen/ijavascript.js、tests/data/notebooks/outputs/ipynb_to_Rmd/ijavascript.Rmd 与 tests/data/notebooks/outputs/ipynb_to_myst/ijavascript.md)都针对同一份 IJavascript notebook 生成了不同格式的镜像,读者可并排对照,观察语言注册表如何在各格式间复用一个内核描述。
五、镜像测试:如何保证转换结果长期稳定
这份ijavascript.md并非一次性手工产物,而是由镜像测试体系自动维护的固定参照。在 tests/functional/round_trip/test_mirror.py 中:
def test_ipynb_to_md(ipynb_file, no_jupytext_version_number): assert_conversion_same_as_mirror(ipynb_file, "md", "ipynb_to_md")测试逻辑如下:
ipynb_filefixture(定义于 tests/conftest.py)会参数化遍历tests/data/notebooks/inputs下的全部输入 notebook,其中就包括ipynb_js/ijavascript.ipynb;assert_conversion_same_as_mirror(实现在 src/jupytext/compare.py)将 notebook 以md格式写出,并把结果与outputs/ipynb_to_md/目录下的镜像文件逐字符比较(compare(actual, expected));- 若镜像文件不存在,
create_mirror_file_if_missing会首次生成之(src/jupytext/compare.py),之后则要求每次转换结果与既有镜像完全一致,从而捕捉任何意外的格式漂移。
同时,no_jupytext_version_numberfixture 会在比较前剥离 Jupytext 版本号等易变字段,保证镜像文件对版本迭代保持稳定。这套"输入 notebook → 多格式镜像 → 逐字节比对"的机制,是 Jupytext 文本格式可靠性的重要防线,也意味着本文解析的样例内容是经过测试锁定的规范行为,而非偶然输出。
六、实战:在本地复现该转换
若你想在自己的 IJavascript notebook 上复现上述转换,可直接使用 Jupytext 的命令行入口(见 src/jupytext/cli.py)。在仓库环境已安装依赖的前提下:
# 将 IJavascript notebook 转换为 Markdown 文档 jupytext --to md ijavascript.ipynb # 指定输出路径(不会覆盖输入文件) jupytext --to md:ipynb_to_md/ijavascript.md ijavascript.ipynb # 反向还原:由 Markdown 文档重建 notebook jupytext --to ipynb ijavascript.mdPython API 等价写法:
import jupytext nb = jupytext.read("ijavascript.ipynb") # 读取 ipynb md_text = jupytext.writes(nb, "md") # 序列化为 Markdown 文本 jupytext.write(nb, "ijavascript.md", fmt="md") # 直接写出文件转换后生成的.md文档即可纳入 Git 版本控制:Markdown 代码块天然可 diff、可评审,团队成员可以直接在 Markdown 中编辑代码与文档,再通过 Jupytext(如 src/jupytext/jupytext.py 提供的配对同步机制)将编辑回写为 notebook。需要提醒的是,输出内容不会进入 Markdown,若需要保留执行结果,仍应以.ipynb为准。
小结
从一份看似简单的ijavascript.md出发,本文还原了 Jupytext 将 IJavascript 内核 notebook 转换为 Markdown 的完整链路:YAML 头部承载内核声明、Markdown 单元格原样迁移、代码单元格进入```javascript围栏代码块、执行输出被有意剥离;而 src/jupytext/formats.py、src/jupytext/languages.py、src/jupytext/cell_to_text.py 与 tests/functional/round_trip/test_mirror.py 则分别提供了格式注册、语言映射、读写实现与稳定性保障。理解这套机制后,你既可以放心地将任意内核的 notebook 以 Markdown 形式纳入版本控制,也可以在遇到格式异常时快速定位到对应的源码模块。
- 开发工具
【免费下载链接】jupytext
Jupyter Notebooks as Markdown Documents, Julia, Python or R scripts
相关推荐
Headlamp 前端 KubeContainer 接口全解:Kubernetes 容器对象的 TypeScript 类型体系与源码实战
Headlamp 前端 KubeContainer 接口全解:Kubernetes 容器对象的 TypeScript 类型体系与源码实战 导读 KubeCont
开发工具LeetCode 201 区间按位与(Bitwise AND of Numbers Range)四种解法精讲:从 O(n) 暴力到 O(1) 位运算,附多语言实现
LeetCode 201 区间按位与(Bitwise AND of Numbers Range)四种解法精讲:从 O n 暴力到 O 1 位运算,附多语言实现
开发工具Jupytext 将 IJavascript 笔记本转换为 MyST Markdown:格式结构与转换原理解析
Jupytext 将 IJavascript 笔记本转换为 MyST Markdown:格式结构与转换原理解析 Jupytext 支持把 Jupyter Not
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考