Notebook 文件(.ipynb)就是一个 JSON,没有二进制、没有加密,记事本能打开。 结构四层:nbformat 版本、metadata、cells、每个格子的 source 和 outputs。 输出也存在文件里,所以会出现「代码改了、输出还是旧的」——这是最常见的错误结论来源。
上一篇装的是运行环境,这一篇讲工作方式。Pandas 之后的所有篇章,原文都是 Notebook 里写的。 本沙箱没装 Jupyter,但装了 nbformat 5.11.1,可以真建一个 .ipynb 并读回来看结构。
一、它和 .py 文件差在哪
.py 文件里只有代码,输出在终端里刷过去就没了。 .ipynb 文件里代码和输出都存在一起,还夹着 markdown 文字。打开就能看到上次跑的结果。 这个特点决定了它的用途:探索。一段代码、一段解释、一张图,像写实验记录。
也决定了它的弱点:不适合当工具交付,不适合进版本控制频繁对比。
二、结构四层
用记事本打开一个 .ipynb,会看到四层结构:
| 层级 | 键 | 内容 |
|---|---|---|
| 1 | nbformat | 格式版本号,本机是 4 |
| 2 | metadata | 内核名字、语言版本 |
| 3 | cells | 一个列表,每个元素一个格子 |
| 4 | source / outputs | 源码字符串、输出列表 |
本篇现场建的那个最小 Notebook:两个格子(一个 markdown、一个代码),文件 386 字节。 386 字节什么概念?比一张缩略图还小。说明它真的只是一段文本。
三、输出为什么会在文件里
因为要「打开就能看」。别人不需要重跑,也能看到你跑出来的图和表。 但这也带来一个副作用:图的输出是 base64 编码的字符串,存在 JSON 里。 所以带图的 Notebook 会突然变大,几十兆很常见。发邮件之前最好先清输出。
清理的办法在菜单里:Restart and Run All 是重跑,Clear All Outputs 是清空。
四、最阴的一个坑
改了代码但没重跑,老输出还在——这时文件里存的图和表,跟你眼前的代码对不上。 这是 Notebook 最常见的一类错误结论来源:看图是上升的,代码算的却是下降,谁也没发现。 防的办法有两个:一是交出去之前一律 Restart and Run All;二是看执行序号。
代码格左边的 In [12] 那个数字就是执行序号。序号不连续,说明中间有格子没跑。 这一条比任何排版讲究都重要:它保证文件里的每一个结论,都是当前代码跑出来的。
五、怎么建第一个 Notebook
装好之后在命令行敲 jupyter notebook,浏览器会打开一个页面,点 New → Python 3。 格子的两种类型要分清:Code 里写代码,Markdown 里写字。Esc 加 M 切 Markdown,Esc 加 Y 切回代码。 Shift + Enter 是跑当前格并跳到下一格,Ctrl + Enter 是跑完留在原地。
本沙箱不执行 Jupyter 页面,这一段照书上点。但文件结构已经用 nbformat 真建过一遍。
六、动手 Demo:Notebook 文件里到底存了什么
用 nbformat 建一个最小的 Notebook:一个标题格、一个代码格,写到磁盘再读回来。 打出文件大小、格式版本、格子数量和类型。
import os
import nbformat
nb = nbformat.v4.new_notebook()
nb.cells.append(nbformat.v4.new_markdown_cell("# 一个最小的 Notebook"))
nb.cells.append(nbformat.v4.new_code_cell("print(1 + 1)"))
path = "demo.ipynb"
nbformat.write(nb, path)
print("文件 %d 字节" % os.path.getsize(path))
back = nbformat.read(path, as_version=4)
print("nbformat %d,%d 个格子" % (back.nbformat, len(back.cells)))
for c in back.cells:
print(" %-9s %s" % (c.cell_type, c.source.split(chr(10))[0][:40]))
实跑输出:
--------------------------------------------------------------------
Notebook 文件里到底存了什么
--------------------------------------------------------------------
① 它就是一个 JSON 文件
--------------------------------------------------------------------
后缀 .ipynb,内容是一段 JSON。用记事本打开能看到全部内容,不是二进制。
下面现场建一个:一个标题单元格,一个代码单元格。
文件建好了,大小 386.0 B。
! 没有隐藏数据、没有加密。改坏了直接文本编辑也能救回来。
② 结构就四层
--------------------------------------------------------------------
最外层:nbformat 版本号(本机是 4)和 nbformat_minor。
第二层:metadata,记内核名字和语言版本。
第三层:cells,一个列表,每个元素是一个单元格。
第四层:每个单元格有 cell_type 和 source,代码格还有 outputs 和 execution_count。
| 层级 | 键 | 内容 |
|---|---|---|
| 1 | nbformat | 4 |
| 2 | metadata | 内核、语言版本 |
| 3 | cells | 2 个单元格 |
| 4 | source / outputs | 源码与输出 |
! 输出的图也是存在 JSON 里的——base64 编码的字符串。所以带图的 Notebook 会突然变大。
③ 为什么这个结构重要
--------------------------------------------------------------------
因为输出存在文件里,所以别人打开就能看到结果,不用重跑。
也正因为如此,Notebook 会出现「输出和代码不一致」的情况:改了代码没重跑,老的输出还在。
这是 Notebook 最常见的一类错误结论来源。交出去之前一定要 Restart and Run All。
! 代码格里的执行序号(In [12])就是用来发现这件事的:序号不连续,说明中间有格没跑。
④ 什么时候别用 Notebook
--------------------------------------------------------------------
要定时跑、要给别人当工具用、要进版本控制频繁对比——这三种情况脚本更合适。
Notebook 的强项是探索:一段代码一段话地试,试完留下结论和过程。
! 探索用 Notebook,交付用脚本。两者不是替代关系。
nbformat.v4 里那几个 new_* 函数就是几种格子的构造器,写起来比手拼 JSON 省事。
读回来的时候用 as_version=4,它会自动升级老格式。
Notebook 的格式版本变过几次,这一步能省掉兼容问题。
30 秒实验
- 把代码格里的 print(1 + 1) 换成一段画图的代码,文件大小会变多少?
- 用记事本打开生成的 demo.ipynb,找到 cells 那一层,看看它的真实结构。
- 想一想:为什么 Notebook 不适合做定时任务?
常见误区
- 改了代码不重跑就交出去:文件里的输出是旧的,结论可能是错的。
- 把 Notebook 当脚本调度:它要交互环境,不适合 cron 定时跑。
- 格子里塞几百行:Notebook 的价值是一段一段,塞成整篇反而失去了优势。
- 不清输出就提交:带图的 Notebook 几十兆,版本库会被拖垮。
自测三问(点开对答案)
1. .ipynb 文件本质是什么? —— 一段 JSON 文本,记录格子内容、输出和元数据。用记事本就能打开。 2. 为什么带图的 Notebook 会突然变大? —— 因为图的输出是 base64 编码后存在 JSON 里的,一张图就能占几十万字符。 3. 怎么发现「输出和代码不一致」? —— 看代码格的执行序号 In [n]。序号不连续,说明有格子没重跑。交出去前一律 Restart and Run All。Notebook 里那些 % 和 ! 开头的行,是 IPython 提供的。下一篇专门讲这三把钥匙。
- ☐ (本篇完,下一篇 2.7 IPython 起步:装、跑、写、Tab 补全)