- 开发工具
【免费下载链接】jupytext
Jupyter Notebooks as Markdown Documents, Julia, Python or R scripts
本指南以 Jupytext 仓库中的真实示例 sas.md 为主线,完整讲解 Jupyter 生态中 SAS 内核笔记本(.ipynb)如何被表示为 MyST Markdown 文档、两种形态之间如何无损互转,以及 jupytext 在底层如何识别与处理 SAS 语言。读完本文,你将能读懂 MyST 格式的 SAS 文本笔记本结构、理解{code-cell}指令与 YAML front matter 的作用,并能用 jupytext 在自己项目中完成.ipynb与.md的转换与镜像同步。
一、从一份 SAS Notebook 到 MyST 文档:示例全景
仓库测试数据中,tests/data/notebooks/inputs/ipynb_sas/sas.ipynb是一个包含 SAS 内核元数据、1 个 Markdown 单元和 4 个代码单元的笔记本,其内容围绕 SAS 基础操作设计:proc sql查询sashelp.cars数据集、宏变量%let/%put的定义与引用、以及一个完整的%macro宏定义与调用。它被 jupytext 转换后的 MyST 表示保存在 sas.md 中,全文如下:
--- kernelspec: display_name: SAS language: sas name: sas --- # SAS Notebooks with jupytext ```{code-cell} proc sql; select * from sashelp.cars (obs=10) ; quit; ``` ```{code-cell} %let name = "Jupytext"; ``` ```{code-cell} %put &name; ``` ```{code-cell} /* Note when defining macros "%macro" cannot be the first line of text in the cell */ %macro test; data temp; set sashelp.cars; name = "testx"; run; proc print data = temp (obs=10); run; %mend test; %test ```这份文件本身就是一篇自洽的“文本笔记本”:它既可以被任何 Markdown 渲染器正常阅读,又可以被 jupytext 反向解析回与原始.ipynb完全等价的 notebook 对象。这正是 MyST 格式(myst)的定位——在可读性与机器可解析性之间取得平衡。
二、MyST 文本笔记本的结构拆解
2.1 YAML front matter:笔记本级元数据
文件开头的---包裹块是文档级元数据(front matter)。对照源码 myst.py 中的myst_to_notebook实现:解析器(基于markdown-it-py的 front matter 插件)会将该 YAML 块整体提取出来,作为 notebook 的顶层metadata写入。本例中的三个字段:
| 字段 | 值 | 含义 |
|---|---|---|
kernelspec.display_name | SAS | JupyterLab 界面中显示的内核名称 |
kernelspec.language | sas | 内核对应的语言标识 |
kernelspec.name | sas | 内核注册名,用于定位sas内核启动器 |
这三个字段与原始 ipynb 的metadata.kernelspec一一对应(可对照 sas.ipynb)。当 MyST 文档被反向读回并交给 Jupyter 运行时,这些信息决定了 notebook 会绑定哪个内核。
值得注意:在该示例的 MyST 版本中,front matter 直接以kernelspec为顶层键;而在 R Markdown(.Rmd)等格式中,同一份元数据会被包进jupyter:命名空间(参见 sas.Rmd 的写法)。这是不同文本格式对 notebook 元数据的组织约定差异,语义完全等价。
2.2{code-cell}指令:代码单元
MyST 使用带指令标注的围栏代码块表示代码单元。{code-cell}正是 myst.py 中定义的CODE_DIRECTIVE常量。解析逻辑(myst.py)按如下方式工作:
- 解析器扫描顶层 token,凡是
fence(围栏块)且其 info 以{code-cell}开头的,即判定为一个代码单元; - 指令中
{code-cell}之后的文本(如{code-cell} sas)会被作为语法高亮语言(lexer)处理;若所有代码单元语言一致,该 lexer 会被写入 notebook 元数据中的jupytext.default_lexer(myst.py); - 围栏内的全部文本行拼接为该代码单元的
source; - 出现在代码单元之前的普通 Markdown 文本会被收集为 Markdown 单元(对应 sas.md 中的
# SAS Notebooks with jupytext标题)。
反方向(notebook → MyST)由notebook_to_myst(myst.py)完成:它遍历 notebook 单元,将代码单元输出为{code-cell}围栏块,并把单元格级元数据以 YAML 或:key: value形式写在指令体内部。这是镜像往返测试能成立的基础。
三、示例中的 SAS 代码单元逐一解读
该示例的四个代码单元覆盖了 SAS 编程的三种典型形态,也顺便验证了 jupytext 对%宏指令文本的原样保留能力。
3.1 SQL 过程步
proc sql; select * from sashelp.cars (obs=10) ; quit;这是 SAS 中通过proc sql直接执行 SQL 查询的典型写法,读取 SAS 自带示例数据集sashelp.cars的前 10 行。文本笔记本中它被原样存储,没有任何转义或改写。
3.2 宏变量定义与引用
%let name = "Jupytext";%put &name;%let定义宏变量,&name引用它,%put将解析后的值打印到日志。%是 SAS 宏语言的关键前缀,也是 jupytext 文本表示中最需要小心的字符之一。源码 magics.py 中有一个针对性处理:对于octave、matlab、sas、logtalk等语言,行首的%不会被误判为需要注释掉的 Jupyter magic——这正是 SAS 宏代码能在文本与 ipynb 之间逐字符往返而不损坏的根本保证。
3.3 宏定义单元(含重要注释)
/* Note when defining macros "%macro" cannot be the first line of text in the cell */ %macro test; data temp; set sashelp.cars; name = "testx"; run; proc print data = temp (obs=10); run; %mend test; %test这个单元示范了两件事:
- SAS 块注释:
.sas语言在 jupytext 中登记的注释符是/*、*/(见下文第 4.1 节),因此示例使用/* ... */书写说明文字; - 宏定义的边界约束:单元内明确指出,定义宏时
%macro不能作为单元文本的第一行——必须在前面先放一条注释。这是 SAS 内核解析文本单元的实操经验,也是该示例特意保留注释的原因:它确保代码单元被真正执行而不是被当作特殊指令处理。
四、源码级深度解析:jupytext 如何认识 SAS
4.1 SAS 在语言注册表中的位置
jupytext 支持的所有脚本语言集中登记在 languages.py 的_SCRIPT_EXTENSIONS字典中。SAS 的登记条目为:
".sas": { "language": "sas", "comment": "/*", "comment_suffix": "*/", },这告诉 jupytext 三件事:.sas扩展名对应的语言名是sas;在 percent / light 等“注释式”文本格式中,行注释使用/* ... */块注释。同时,sas也出现在_JUPYTER_LANGUAGES列表(languages.py)中,意味着它既可作为 notebook 的主语言,也可作为单元格级 magic 语言被识别。此外 languages.py 的usual_language_name把"sas"规范化为"SAS",用于与内核声明的语言做大小写无关的等价比较。
4.2 主语言判定与单元格语言
default_language_from_metadata_and_ext 决定文本笔记本的主语言:优先取metadata.jupytext.main_language,其次取metadata.kernelspec.language,最后回退到扩展名映射。SAS 与 R 一样被单独列出、直接返回原值而不做小写化——这保证kernelspec.language: sas在转换中不会被改写。当 notebook 没有设置内核时,set_main_and_cell_language 会统计各单元格语言、取多数者为main_language并写入jupytext元数据,确保后续其他格式的转换仍然稳定。
4.3 MyST 格式的完整解析/生成流程
MyST 的读写主逻辑都在 myst.py 中:
- 读取方向(
myst_to_notebook,myst.py):用markdown-it-py配合front_matter_plugin、myst_block_plugin、myst_role_plugin解析文本 → 提取 front matter 为 notebook 元数据 → 按 token 顺序把普通 Markdown 文本 flush 为 Markdown 单元、把{code-cell}/{raw-cell}围栏块解析为代码/原始单元 → 支持+++分块标记承载 Markdown 单元元数据。若解析 YAML 失败会抛出MystMetadataParsingError并附带行号定位(myst.py)。 - 写出方向(
notebook_to_myst,myst.py):将 notebook 元数据经dump_yaml_blocks(myst.py)压缩为 YAML 块;代码单元写成{code-cell}围栏,且会智能选择不少于三反引号的围栏分隔符(three_backticks_or_more)以避免与单元内容中的反引号冲突。
当.md文件同时满足“以---开头 + 含{code-cell}指令”等特征时,matches_mystnb 会判定其为 MyST 文本笔记本——这正是 jupytext 在打开文件时自动选择myst格式的依据。
五、同源多格式对照:一份 SAS Notebook 的多种文本形态
仓库的 outputs 目录保存了同一份 SAS ipynb 在不同文本格式下的镜像输出,是理解 jupytext 格式矩阵的最佳材料:
| 格式 | 输出文件 | 代码单元表示 | 元数据表示 |
|---|---|---|---|
| MyST Markdown | ipynb_to_myst/sas.md | {code-cell}围栏块 | 顶层 YAML front matter |
| Markdown | ipynb_to_md/sas.md | 普通 ```sas 围栏块 | YAML front matter |
| R Markdown | ipynb_to_Rmd/sas.Rmd | {sas}围栏块 | jupyter:命名空间下的 front matter |
| Percent 脚本 | ipynb_to_percent/sas.sas | /* %% */单元分隔注释 + 块注释/* ... */ | 块注释包裹的 YAML |
| Hydrogen 脚本 | ipynb_to_hydrogen/sas.sas | /* %% */分隔注释 | 块注释包裹的 YAML |
| Light 脚本 | ipynb_to_script/sas.sas | 空行分隔 | 块注释包裹的 YAML |
以 ipynb_to_percent/sas.sas 为例,可以看到.sas语言下的“注释式”写法:由于 SAS 的注释符是/*、*/(来自 4.1 节的注册表),jupytext 用/* %% */标记单元边界、用/* --- */ ... /* --- */包裹 YAML 元数据,而%let、%put、%macro等宏代码行则完全原样保留、不做注释化处理(与 3.2 节的 magic 豁免逻辑一致)。
六、测试如何保障 SAS 往返无损
SAS 示例文件的生成与验证由镜像测试体系保障。test_mirror.py 中的test_ipynb_to_myst对所有tests/data/notebooks/inputs下的 ipynb(包括ipynb_sas/sas.ipynb)执行:
@pytest.mark.requires_myst def test_ipynb_to_myst(ipynb_file, no_jupytext_version_number): assert_conversion_same_as_mirror(ipynb_file, "md:myst", "ipynb_to_myst")assert_conversion_same_as_mirror会做两件事:一是把 ipynb 转成 MyST 文本并与outputs/ipynb_to_myst/目录下的基准文件逐字节比对;二是把转换结果再读回 ipynb,验证与原 notebook 等价。这两个方向共同保证“文本是 ipynb 的忠实镜像”,防止新版本发布时产生无意的格式漂移。同一文件还参与test_ipynb_to_md、test_ipynb_to_Rmd、test_ipynb_to_percent等其它格式的镜像测试(test_mirror.py),因此 SAS 示例在多格式间的行为都被持续回归监控。
七、在自己的项目中复现与使用
查看示例:直接阅读 sas.md 了解 MyST 文本笔记本的排版规范;对照 sas.ipynb 观察两种形态的对应关系。
命令行转换(在已安装 jupytext 与
markdown-it-py的环境中):
# ipynb -> myst(md 文件带 {code-cell} 指令即视为 myst 格式) jupytext --to md:myst sas.ipynb -o sas.myst.md # myst -> ipynb jupytext --to ipynb sas.myst.md -o sas.ipynbmd:myst中的myst是格式名(myst_extensions允许.md、.myst、.mystnb、.mnb扩展名,见 myst.py);MyST 格式要求环境提供markdown-it-py及三个 mdit 插件(myst.py),否则会抛出明确的导入错误提示。
- 配对同步:为让 ipynb 与 MyST 文本保持实时镜像,可在
jupytext配置中声明配对格式,例如在jupytext.toml中设置formats = "ipynb,myst",此后 Jupyter 中保存 notebook 会自动同步写两份文件;文本文件的编辑也会触发 ipynb 的更新。这也是本仓库 demo 目录(如demo/World population.myst.md)所演示的日常工作流。
八、小结
通过 sas.md 这一示例可以看到,jupytext 对 SAS 笔记本的 MyST 化表达已经形成完整闭环:语言注册表(languages.py)负责识别.sas扩展名与sas内核、豁免%宏指令被误注释(magics.py);MyST 模块(myst.py)负责 front matter、{code-cell}指令与单元格的双向解析/生成;镜像测试(test_mirror.py)则持续验证 ipynb ↔ MyST 之间不丢失任何单元格、元数据与 SAS 宏代码。对于需要在 Git 中版本管理 SAS 分析流程、或用 Markdown 撰写可执行的 SAS 文档的团队,这套文本表示提供了既适合人读、又适合机器往返的中间形态。
- 开发工具
【免费下载链接】jupytext
Jupyter Notebooks as Markdown Documents, Julia, Python or R scripts
相关推荐
Jupytext 实战:将 Octave 笔记本转换为 MyST Markdown 文档
Jupytext 实战:将 Octave 笔记本转换为 MyST Markdown 文档 导读:本文以 Jupytext 仓库中的真实转换产物 tests/da
开发工具Jupytext 实战:将含 Plotly 交互图表的 Notebook 转换为 MyST Markdown 轻量文档
Jupytext 实战:将含 Plotly 交互图表的 Notebook 转换为 MyST Markdown 轻量文档 本指南以 Jupytext 仓库中的真实
开发工具Jupytext 实战:将含文本、HTML、图片与错误输出的 Notebook 转换为 MyST Markdown
Jupytext 实战:将含文本、HTML、图片与错误输出的 Notebook 转换为 MyST Markdown 本文以 Jupytext 仓库中的真实转换产
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考