- 开发工具
【免费下载链接】jupytext
Jupyter Notebooks as Markdown Documents, Julia, Python or R scripts
Jupytext 允许把 Jupyter Notebook 以纯文本形式保存、版本控制与协作编辑,其中.md(Markdown)格式尤其适合面向文档的笔记本。本文以仓库测试数据中真实的转换产物 root_cpp.md 为核心样本,逐行讲解 Jupytext 如何把一份运行于 ROOT C++ 内核的.ipynb笔记本转换为 Markdown 文本笔记本,并结合 formats.py、header.py、cell_reader.py 等源码揭示其底层解析与写出机制。读完本文,你将掌握 Markdown 文本笔记本的完整语法(YAML 头、代码围栏、语言标签)、jupytext --to md等命令行用法,以及如何用源码和测试验证转换行为。
一、样本背景:一份来自 ipynb_to_md 回归测试的产物
root_cpp.md是 Jupytext 仓库tests/data/notebooks/outputs/ipynb_to_md/目录下的一个转换输出。该目录集中存放了大量“ipynb → Markdown”转换的期望结果,覆盖 R、C++、Julia、C#、F#、Go、Java、JavaScript、Scala、Wolfram 等数十种语言,root_cpp.md只是其中 C++ 语言的代表样本。
它对应的输入是 tests/data/notebooks/inputs/ipynb_cpp/root_cpp.ipynb:一份包含 3 个代码单元格的 C++ 笔记本,其kernelspec声明为:
"kernelspec": { "display_name": "ROOT C++", "language": "c++", "name": "root" }其中name: root对应 CERN ROOT 数据分析框架的 Jupyter 内核(ROOT 内置 C++ 解释器,可直接在单元格中书写 C++ 代码)。输入笔记本的第三个单元格还带有真实执行输出:
k = 4 This string says "foo"而转换出的 root_cpp.md 全文只有 22 行,没有任何输出内容。这恰恰是 Markdown 文本笔记本的核心设计:文本笔记本只保存输入(以及可选的元数据),不保存执行输出。输出只存在于.ipynb文件中,通过配对(paired notebook)机制在重新打开时恢复——这一点在 README.md 的 Paired Notebooks 一节中有明确说明。
二、逐行拆解 root_cpp.md:Markdown 文本笔记本的三种构件
将root_cpp.md与源笔记本对照,可以清晰看到 Jupytext Markdown 格式的组成:
2.1 YAML 文档头(front matter):保存笔记本级元数据
文件开头两行是 YAML front matter:
--- jupyter: kernelspec: display_name: ROOT C++ language: c++ name: root ---它由一对---定界行包裹,内容为jupyter命名空间下的笔记本级元数据。在源码层面,header.py 用正则_HEADER_RE = re.compile(r"^---\s*$")识别头部的起止行,并用_JUPYTER_RE = re.compile(r"^jupyter\s*:\s*$")识别jupyter:命名空间。转换时 metadata_and_cell_to_header 会从笔记本元数据中提取 kernelspec 等信息写入头部;回读时则反向解析,把 front matter 还原为notebook.metadata["jupyter"]["kernelspec"]。
从源码结构看,凡是与 Jupyter 运行环境相关的元数据(kernelspec、language_info 等)都会归入jupyter命名空间并出现在头部,而jupytext命名空间(如text_representation,记录扩展名、格式名、格式版本与 jupytext 版本)通常只在命令行实际转换时写入。测试环境为了稳定比对,会关闭版本号插入(见 header.py 的INSERT_AND_CHECK_VERSION_NUMBER开关),这正是仓库测试产物头部只有jupyter.kernelspec而没有jupytext.text_representation的原因。
2.2 代码围栏(fenced code block):代码单元格的载体
头部之后是三个以 ``` 包裹的代码块,每个围栏都带语言标识c++:
#include <iostream> #include <string>int k = 4; std::string foo = "This string says \"foo\"";std::cout << "k = " << k << '\n' << foo << '\n';对照源.ipynb,这正好对应它的三个code单元格(execution_count分别为 1、2、3,单元格顺序与 ID 一一对应)。Jupytext 的 Markdown 读取器(MarkdownCellReader)在解析时,把带语言信息(或默认语言)的围栏代码块识别为代码单元格;不带语言标识的围栏代码块或普通段落则被识别为 Markdown 单元格或 raw 单元格。单元格之间的空行用于分隔,围栏内内容原样保留。
这里有两个值得注意的细节:
- 字符串转义被完整保留:第二个单元格中的
std::string foo = "This string says \"foo\"";,其\"转义在.ipynb中存储为\\"(JSON 层转义),落到.md中还原为源码原文\",说明 Jupytext 对单元格源码是“逐字搬运”,不做任何改写。 - 换行语义一致:每个单元格的源码(如
#include <iostream>\n#include <string>)在.md中以多行形式呈现,与.ipynb中source数组逐行对应,回读时会被原样重组为单条source字符串。
2.3 输出被剔除:文本笔记本的输入优先原则
输入笔记本第三个单元格携带的 stdout 输出(k = 4/This string says "foo")在root_cpp.md中完全不存在。这是 Jupytext 所有文本格式(markdown、percent、light、myst 等)的统一约定:文本文件只承载输入与元数据。由此带来的直接收益是 Git diff 可读——对.md或.py笔记本的改动就是普通文本 diff,这正是 README 反复强调的版本控制场景。
若要保留输出,则采用配对模式:让.ipynb与.md成对存在,文本文件管输入、.ipynb管输出(详见下文第六节)。
三、Markdown 格式在 Jupytext 中的官方定义
root_cpp.md使用的“经典 Markdown 格式”在 formats.py 中有明确注册:
NotebookFormatDescription( format_name="markdown", extension=".md", header_prefix="", cell_reader_class=MarkdownCellReader, cell_exporter_class=MarkdownCellExporter, current_version_number="1.3", min_readable_version_number="1.0", ),从源码注释可以读出该格式的演进历史:
- 1.0(2018-08-31,jupytext v0.6.0):初始版本;
- 1.1(2019-03-24,jupytext v1.1.0):支持 Markdown 区域(
<!-- #markdown -->/<!-- #endmarkdown -->)与单元格元数据; - 1.2(2019-09-21,jupytext v1.3.0):raw 区域改用 HTML 注释编码,单元格元数据默认采用
key=value表示; - 1.3(2021-01-24,jupytext v1.10.0):代码单元格允许以超过三个反引号开头(用于代码内容本身含三个反引号的场景)。
current_version_number="1.3"意味着当前写出的 Markdown 文本符合 1.3 版规范;min_readable_version_number="1.0"则允许读取 1.0 及以后的所有历史版本,保证向前兼容。另外.markdown扩展名也注册了同一markdown格式(formats.py),只是版本号停在 1.2。
值得说明的是,.md扩展名在 Jupytext 中并非只属于“markdown”格式:MyST 与 Pandoc 也使用.md扩展名(如md:myst、md:pandoc格式别名,见 formats.py)。当用户直接读取.md文件时,Jupytext 会依据文件内容自动判定具体格式——例如是否以---开头、是否包含{code-cell}指令等(myst.py 的matches_mystnb就是这样的探测逻辑)。root_cpp.md采用普通围栏代码块而非 MyST 指令,因此被判定为经典markdown格式。
四、底层原理:读取与写出链路
4.1 写出(ipynb → md):单元格逐一分发到 Markdown 构件
当执行jupytext --to md时,核心流程是:读取.ipynb→ 依据目标格式调用 MarkdownCellExporter 将每个单元格序列化为文本 → 调用 header.py 生成/合并 YAML 头 → 拼装成.md文件。三个代码单元格被写成三个围栏代码块,头部由metadata_and_cell_to_header从笔记本元数据提取jupyter.kernelspec生成,输出则被过滤丢弃。
4.2 读取(md → ipynb):cell_reader.py的解析逻辑
回读方向由 cell_reader.py 负责。对于.md/.markdown扩展名,读取器初始化逻辑会依据扩展名选择默认语言;遇到围栏代码块时,围栏语言信息(本样本为c++)与默认语言共同决定单元格的代码语言属性。该文件还实现了 Markdown 区域、raw 区域(<!-- #raw -->注释)、.noeval属性等扩展语法,但这些在root_cpp.md中未出现——样本刻意保持最小化,恰好展示最朴素的“纯代码单元格”场景。
4.3 语言归一化:c++如何被识别
root_cpp.md的围栏语言标签写作c++,而 ROOT 内核的display_name是ROOT C++。这背后是 languages.py 的语言归一化逻辑:该模块定义了脚本扩展名与注释符号的映射(.cpp对应//注释,见 languages.py),并提供了大小写/写法归一化(以C++或c++开头的语言名统一归为c++,见 languages.py)。因此无论内核以何种写法声明,围栏语言标签都能稳定输出为标准小写c++,这也是.md文件在 GitHub、VS Code 等工具中能获得正确语法高亮的前提。
五、实战:在命令行复现 root_cpp.md 的转换
以仓库测试数据为例,你可以在本地复现这一转换:
# 安装 Jupytext(在 Jupyter 所在 Python 环境) pip install jupytext # 或:conda install jupytext -c conda-forge # 将 C++ 笔记本转换为 Markdown 文本笔记本 jupytext --to md tests/data/notebooks/inputs/ipynb_cpp/root_cpp.ipynb -o root_cpp.md # 反向转换:把 Markdown 文本还原为 ipynb jupytext --to ipynb root_cpp.md -o root_cpp.ipynb # 直接输出到标准输出查看文本表示 jupytext --to md --output - tests/data/notebooks/inputs/ipynb_cpp/root_cpp.ipynb相关命令行行为在 tests/functional/cli/test_cli.py 中有成体系的测试覆盖,包括-o指定输出文件、--to指定目标格式、管道处理等。转换后的root_cpp.md会与仓库中的期望产物逐字节一致(版本号插入在测试中关闭),这正是回归测试的意义:任何对 Markdown 格式写出的改动都会被这些快照捕获。
六、配对使用:让 C++ 笔记本既好协作、又保输出
root_cpp.md这类文本笔记本适合版本控制与 IDE 编辑,但缺输出。Jupytext 推荐的完整方案是配对笔记本——.ipynb与.md同时存在、互相同步:
# 在 Jupyter 中为笔记本声明配对格式 jupytext --set-formats ipynb,md root_cpp.ipynb # 之后任意一端更新后,用同步命令让两者保持一致 jupytext --sync root_cpp.md工作流为:在 Jupyter 中保存笔记本时,Jupytext 自动把输入写入.md;在 IDE 中编辑.md后,Jupyter 侧“从磁盘重新加载笔记本”,输入来自.md、输出从.ipynb恢复。若想对整个目录生效,可在目录根放置 jupytext 配置文件:
# jupytext.toml formats = "ipynb,md"这样一来,ROOT C++ 笔记本既能在 IDE 中获得清晰 diff、又能保留全部计算输出。
七、回读验证与测试保障
仓库对 Markdown 格式的回读能力有专门测试:tests/functional/simple_notebooks/test_read_simple_markdown.py。该测试用jupytext.reads(markdown, "md")读取一段 Markdown 文本,断言解析出的单元格类型、语言与元数据,再用jupytext.writes(nb, "md")写回并用compare断言往返一致(round-trip)。其中覆盖了“以 Python 为主体的 Markdown 文件”“Markdown 区域”“raw 区域”“无语言信息的围栏代码块”“R 围栏代码块”等多种场景。
对于root_cpp.md这类带 YAML 头的文件,读取方向的核心断言点包括:front matter 中的jupyter.kernelspec被还原到nb.metadata、围栏语言c++被正确记录、三个围栏块被解析为三个独立代码单元格。结合 myst_to_ipynb 与 md_to_ipynb 目录下的反向产物,仓库事实上对“md ↔ ipynb”双向转换都固化了期望快照。
八、小结:从 root_cpp.md 看 Jupytext Markdown 格式的通用规律
root_cpp.md虽小,却浓缩了 Jupytext Markdown 文本笔记本的全部核心规则:
| 构件 | 语法 | 对应 ipynb 内容 | 源码依据 |
|---|---|---|---|
| YAML front matter | ---定界 +jupyter.kernelspec | 笔记本级元数据 | header.py |
| 代码单元格 | ``` + 语言标签的围栏块 | 每个codecell 的source | formats.py、cell_reader.py |
| 语言标签 | 归一化为小写(如c++) | kernelspec.language | languages.py |
| 执行输出 | 不写入 | ipynb 的outputs | README 配对笔记本说明、输出目录快照对比 |
无论你的笔记本运行在 Python、R、Julia 还是 ROOT C++ 内核,只要遵循这套规则,Jupytext 就能稳定完成 ipynb ↔ md 双向转换。若想深入验证,建议直接在本仓库运行对应的回归测试,并参考 test_read_simple_markdown.py 亲手构造自己的往返用例。
- 开发工具
【免费下载链接】jupytext
Jupyter Notebooks as Markdown Documents, Julia, Python or R scripts
相关推荐
Jupytext 实战:将 .NET C 交互式笔记本转换为 MyST Markdown 文本格式
Jupytext 实战:将 .NET C 交互式笔记本转换为 MyST Markdown 文本格式 Jupytext 的核心能力是让 Jupyter 笔记本以可
开发工具Jupytext Markdown 文本笔记本格式解析:从 .ipynb 到 .md 的转换、编码规则与往返验证实战
Jupytext Markdown 文本笔记本格式解析:从 .ipynb 到 .md 的转换、编码规则与往返验证实战 导读 Jupytext 的核心能力之一是把
开发工具Jupytext 转换 Robot Framework 笔记本为 Markdown:格式规范、转换链路与镜像测试验证
Jupytext 转换 Robot Framework 笔记本为 Markdown:格式规范、转换链路与镜像测试验证 导读 本文围绕 jupytext 项目中
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考