Jupyter Notebook 转 Markdown:留下正文,扔掉那层 JSON 外壳
一个 notebook 是两份文档共用一个文件名。一份是你写的东西——标题、说明、代码、那条让论点成立的运行结果;另一份是这种格式用来装它的 JSON 外壳。用文本编辑器打开一个 .ipynb,你看到的就是外壳:每一行正文被拆成单独的字符串,每个单元格都挂着 "execution_count": 7,到处是空的 "metadata": {} 对象,还有夹在中间的四分之一兆字节 base64——渲染出来不过是一张小散点图。
转成 Markdown 就是把外壳丢掉、把文档留下。你的 markdown 单元格本来就是 Markdown,原样通过;代码单元格变成带内核语言标记的围栏代码块,而这正是所有模型、所有 README、所有静态站点生成器早就认识的形式。把 .ipynb 拖到上面的工具里,转换全程在这个浏览器标签页里完成——文件不会被上传。
体积到底压在哪里
很多人以为 notebook 大是因为代码多。几乎从来不是。体积集中在四个地方,按份量从大到小:
- base64 图片输出。每一次
plt.show()都会把渲染好的 PNG 以 base64 文本写进文件。一张图通常就是 200 到 400 KB 的字符,十几张图的 notebook 里大部分内容都是图。对读文本的人来说,这些字符没有任何意义。 - DataFrame 的 HTML 表示。Pandas 对同一个结果会同时输出
text/plain和text/html。HTML 版本带着内联样式和一个每格一对标签的<table>,体积可以是纯文本版的二十倍,说的却是同一件事。 - 进度条的每一帧。tqdm 每刷新一次就写一行,中间用回车符分隔。于是文件里躺着几百份同一条进度条的副本,而它在屏幕上只出现过一次。
- 逐单元格的元数据。单看微不足道,加起来不是:单元格 id、执行计数、折叠标记、空的 metadata 对象,乘以几百个单元格就很可观。
上面的转换逐条处理这些。媒体负载会被统计并替换成一行占位,你仍然看得出这里原本有张图。当一个结果同时提供纯文本和 HTML 时,取纯文本。回车符序列折叠成该行的最终状态。元数据整块丢弃。工具会报告它删掉了什么,省下的体积是看得见的,不是嘴上说的。
为什么带语言标记的围栏代码块比看上去更重要
Markdown 为 notebook 做的最有用的一件事,是把正文和代码的边界显式地标出来。在原始 JSON 里,这条边界只以内容上方几行的一个 "cell_type" 字段存在。粗暴展平之后,一段说明和它所描述的代码会连成一片,读者——无论是人还是模型——只能从语法本身去猜哪段是哪段。
带语言标记的围栏消除了这种猜测,同时把内核语言一路带下去:转换器会读 notebook 元数据里的 language_info,没有就退回 kernelspec,所以 R 或 Julia 的 notebook 会被标成 R 或 Julia,而不是被默默当成 Python。输出放在产生它的单元格下方、自己的无标记围栏里,前面加一行朴素的 Output:,因果关系仍然看得见,又不必发明 Markdown 里没有的语法。
报错回溯值得留下
有人会顺手把错误输出和别的东西一起剥掉。这通常是错的。如果你把 notebook 交给模型、问它某个单元格为什么失败,回溯就是整个问题本身。原始形态的回溯之所以难看,问题不在内容,而在 IPython 给它裹上的 ANSI 颜色码——那些转义序列在终端里是好看的颜色,在别的任何地方都是 ESC[0;31m 这样的乱码。
这些序列会被剥掉,回溯文本保留。过长的回溯从中间截断而不是从尾部截断,因为深栈里有用的是最前面几帧和最后几帧,中间那两百行库内部调用本来就是你会跳过的部分。
把 ipynb 转 Markdown 放进仓库导出
GitHub、GitLab 和本地文件夹工具内部跑的是同一套转换。在仓库里勾选一个 .ipynb,它会以可读的单元格形式落到输出里,而不是一堵 JSON 墙。这件事比听起来重要:在数据科学仓库里,真正的推理过程常常就写在 notebook 里,而在此之前,它们恰恰是你不得不取消勾选、才能让输出还能用的那些文件。
在那个场景下,输出长度限制比本页更紧,因为仓库导出是喂给模型的上下文,而单元格输出是其中每 token 价值最低的部分。转换后的文本里会有一条注记,记录删掉了多少,不会有东西悄无声息地消失。
老版本的 notebook 照样能读
格式版本 4 把单元格放在顶层的 cells 数组里。版本 3 仍散落在 2010 年代初的公开仓库中,它把单元格多嵌一层放进 worksheets,并把源码字段叫 input 而不是 source。两种形状都能读。版本 3 用 pyout 这个输出类型来表示版本 4 里的 execute_result,同样支持。
有两件事是刻意不做的。Notebook 小部件——由 ipywidgets 驱动的交互滑块和图表——状态存在另一块元数据里,转成文本没有任何有用的呈现,占位行会告诉你这里曾经有一个。另外,单元格的执行顺序按它在文件里出现的样子保留,不按执行计数重排,因为你阅读 notebook 的顺序就是它被写下来的顺序。
没有任何东西被上传
Notebook 里常常留着人们早就忘掉的东西:调试时粘进单元格的 API 密钥、数据库连接串、为了检查 join 是否正确而打印出来的一片生产数据。这里的转换由这个标签页里的 JavaScript 完成。没有上传步骤,没有服务器副本,事后也不需要请求删除。关掉标签页,它就没了。
如果你要的是纯文本而不是 Markdown——不要围栏、不要结构,只要文字和代码——notebook 转纯文本工具可以做到,而且你已经选好的文件会一并带过去。