📊 第2章 搭建 Python 数据分析环境
📄 一句话概括

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 秒实验

  1. 把代码格里的 print(1 + 1) 换成一段画图的代码,文件大小会变多少?
  2. 用记事本打开生成的 demo.ipynb,找到 cells 那一层,看看它的真实结构。
  3. 想一想:为什么 Notebook 不适合做定时任务?

常见误区

  1. 改了代码不重跑就交出去:文件里的输出是旧的,结论可能是错的。
  2. 把 Notebook 当脚本调度:它要交互环境,不适合 cron 定时跑。
  3. 格子里塞几百行:Notebook 的价值是一段一段,塞成整篇反而失去了优势。
  4. 不清输出就提交:带图的 Notebook 几十兆,版本库会被拖垮。
自测三问(点开对答案) 1. .ipynb 文件本质是什么? —— 一段 JSON 文本,记录格子内容、输出和元数据。用记事本就能打开。 2. 为什么带图的 Notebook 会突然变大? —— 因为图的输出是 base64 编码后存在 JSON 里的,一张图就能占几十万字符。 3. 怎么发现「输出和代码不一致」? —— 看代码格的执行序号 In [n]。序号不连续,说明有格子没重跑。交出去前一律 Restart and Run All。

Notebook 里那些 % 和 ! 开头的行,是 IPython 提供的。下一篇专门讲这三把钥匙。

相关笔记