说个真实感受:我最初接触 Jupyter Notebook 时,一直把 .ipynb 当成一个普通的“Python 解释器窗口”,直到某天需要把几十个实验报告批量整理成脚本,才被这文件的内部结构狠狠教育了一课。这一课的核心就是:.ipynb 本质上是一个有着严格格式的 JSON 文件,不理解它的内部结构,你就永远只能依赖图形界面点点点,一旦遇到需要批量处理、代码审查、版本管理、自动化提取的场景,整个人都会卡在第一步。
这篇文章不是什么泛泛的概念介绍,我打算从 .ipynb 的文件本质出发,逐层拆开它的 JSON 组织方式,然后用真实的读写代码演示怎么批量操作、怎么与 .py 互相转换、怎么在版本控制里减少冲突,最后把这几年的踩坑经验整理成一个常见问题清单。不管你是刚开始用 Jupyter 的新手,还是已经被 notebook 的 diff 折磨过无数次的开发老手,这篇内容应该都能帮你把“文件”这件事彻底想明白。
1. .ipynb 到底是什么?为什么 Jupyter 生态绕不开它
1.1 从一段最简单的 notebook 看起
先别急着背概念,我们直接打开任意一个 .ipynb 文件看一下。你用任何纯文本编辑器打开它,看到的不是加密内容,也不是编译产物,而是一大坨 JSON。我随便新建一个 notebook,里面只写了一句 print("hello"),大概就是这个样子:
json复制{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "cell-1a2b3c",
"metadata": {
"execution": {
"iopub.execute_input": "2024-01-01T00:00:00.000Z",
"iopub.status.busy": "2024-01-01T00:00:00.000Z",
"iopub.status.idle": "2024-01-01T00:00:00.000Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"hello\n"
]
}
],
"source": [
"print('hello')"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.11.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
注意这个结构,它不是一个 Python 脚本那样按行执行的文件,而是一棵描述性的 JSON 树。Jupyter 在打开时把这棵 JSON 树渲染成网页界面,在保存时又把你所有的编辑、运行结果、执行顺序全部序列化回 JSON。
理解了这一点,很多“反直觉”的现象就有了解释。比如为什么同一个 notebook 换一个人打开,代码运行结果的打印样式会变?因为 output 是保存在 cells 里的,跟你本地的 Python 环境没关系。又比如为什么用 Git 看 diff 时经常满屏都是乱码式的变化?因为内核的元数据、执行时间戳、base64 图片这些都在变化,本质上都是这个 JSON 文件在变。
1.2 .ipynb 与 .py 的定位差异
很多人会问:明明 Python 脚本用 .py 就够了,为什么还需要 Jupyter Notebook?我的答案是,它们解决的问题不一样。.py 文件描述的是“静态的、可执行的逻辑”,而 .ipynb 文件描述的是“一次交互探索的过程”。Notebook 允许你在每个 cell 里保存中间结果、Markdown 笔记、可视化图片,这些内容混排在一个文件里,非常适合做数据分析、教学演示、快速实验。
但这种便利也带来了代价。.ipynb 没法直接 import,没法直接在命令行用 python xxx.ipynb 运行,没法像普通模块那样被其他代码引用。尤其是你在写一个完整项目时,如果满脑子只有 notebook,等到要上生产环境、做自动化调度,你会发现自己被绑在了网页界面上。
所以真正成熟的用法,不是不用 notebook,而是理解它和常规脚本之间的边界,做到该用 notebook 时用 notebook,该转脚本时能快速转出去。后面我会专门讲怎么用代码做双向转换。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 逐层拆解 .ipynb 的 JSON 结构
2.1 最外层:五个核心字段
.ipynb 的根节点本质上只有五个字段:nbformat、nbformat_minor、metadata、cells、还有极少数情况下会出现的 signing 字段。
nbformat 表示主版本号,绝大多数 notebook 是 4,你几乎不会看到 3 或者更老的版本。nbformat_minor 是次版本号,它会影响解析器对某些字段的容忍程度,比如 nbformat 4.5 之后对 cell id 有更严格的校验,4.4 之前甚至没有强制要求每个 cell 都有 id。如果你手动编辑过这个字段,千万要注意:版本写得太新,老版本 Jupyter 可能打不开;版本写得太老,一些新字段又可能被忽略。
metadata 是整个文件的配置信息,里面通常包含 kernelspec 和 language_info。前者记录了运行内核的名字和显示名称,比如 python3;后者记录了语言名称、版本号、解释器路径等环境相关信息。这些字段影响的是“你打开 notebook 时会自动选择哪个内核”,但并不会把你的代码限定死,换一个环境打开时,如果你没有装对应的内核,Jupyter 会提示你选择替代内核。
cells 则是整个文件的核心中的核心,它是一个数组,数组里每一项就是一个 cell。所有你能想到的代码块、文字块、图表输出,都在这儿。
我在实际项目里为了搞清楚哪些 notebook 依赖了特定包,会写一个小脚本来读取所有文件的 language_info 和 kernelspec,再用正则扫描源码里的 import 语句,这比人工一个个打开看快太多。后面读取示例的时候,你会看到这一步其实非常简单。
2.2 cells 数组:三种最常见的 cell 类型
每一个 cell 对象里必然有一个 cell_type 字段,它的值决定了这个 cell 的用途。我平时打交道最多的是这三种:code、markdown、raw。
code cell 是代码单元,它包含 source、execution_count、outputs、metadata、id。execution_count 表示这个 cell 在最近一次执行时的编号,如果从没执行过,这个值会是 null。这个字段经常在 Git diff 里制造噪音,因为只要执行顺序变化,它就会跟着变。outputs 数组是 code cell 独有的,用来保存运行结果。
markdown cell 是文字说明单元,它只有 source 和 metadata,以及可选的 attachments 字段。attachments 用于保存你在 Markdown 里拖进去的本地图片,以 base64 字符串形式存储。一个看起来不大的 Markdown 图片,可能在文件里占几百 KB,这也是 notebook 体积膨胀的重灾区之一。
raw cell 是不经过渲染、也不被执行的原始文本,它的存在感很低,通常只在用 nbconvert 导出为 LaTeX 或 HTML 时有点用处。但对于我们做文件解析的人来说,处理方式上跟 markdown 大同小异。
如果你进一步深挖,会发现 code cell 里的 source 并不是一个简单的字符串,而是一个字符串数组。Jupyter 官方格式允许 source 是字符串,也允许是字符串列表,列表中的每个元素代表一行源码。为什么会有这种设计?因为一些历史实现为了让文件增量保存更高效,会把“按行拆分”的文本片段存进数组里。读取的时候最稳妥的做法是调用 "".join(cell["source"]),如果它已经是字符串也不会出错。
2.3 outputs 的四种形态,水很深
code cell 里的 outputs 数组是本文件格式最容易让人踩坑的地方。至少有以下几种典型输出类型,我们逐个说清楚:
第一种是 stream,它表示标准输出或标准错误,字段里有 name,值一般是 stdout 或 stderr,text 字段是输出内容。这是你运行 print 以后的常见结果。
第二种是 execute_result,它表示 cell 最后一行表达式的返回值。这个输出除了 data 和 metadata 之外,还有一个 execution_count 字段。注意,data 里常常是同一种数据的不同 MIME 展现形式,比如 text/plain、text/html、image/png,这也就是为什么你在网页里能看到漂亮的表格或图形。
第三种是 display_data,它表示通过 display() 方法主动展示的内容,结构与 execute_result 相似,但没有执行编号。
第四种是 error,它保存了 traceback 的原始文本信息,字段包括 ename、evalue、traceback。这玩意儿一旦混入正常的 Git 审查里,基本就是一场灾难,因为 traceback 往往很长,包含大量路径信息,diff 一眼望不到头。
理解这些 output 类型不是为了考试,而是为了搞清楚两个实际问题:第一,为什么 notebook 文件动不动就几 MB?因为 image/png 的输出会以 base64 形式直接内嵌到 JSON 里;第二,为什么你用 Git diff 查看变化时一片混乱?因为只要一个 cell 重新运行,outputs 里的时间戳和执行编号都会变。
3. 真实动手:用代码读写 .ipynb 文件
3.1 用 nbformat 读取与保存文件
既然 .ipynb 是 JSON,理论上你用 json.load() 就能读它。确实可以,但我不建议你在项目里直接这么做,因为 Jupyter 生态提供的 nbformat 库已经帮我们做了很多容错和版本兼容处理。拿它读文件只需要这么几行:
python复制import nbformat
nb = nbformat.read("example.ipynb", as_version=4)
print(nb.nbformat)
print(len(nb.cells))
for i, cell in enumerate(nb.cells):
print(i, cell.cell_type, cell.source[:50].replace("\n", " "))
这里 nbformat.read 会自动判断文件的 nbformat 版本,然后返回一个对象。as_version=4 是让旧版本文件统一转换成最新规范,避免后续操作时遇到字段缺失的尴尬。
保存也不是普通 json.dump。用 nbformat.write 能确保写入时缩进、排序、版本字段都符合规范:
python复制nbformat.write(nb, "output.ipynb", version=4)
有些老代码里会用 nbformat.writes 拿到字符串,再手动存文件。两种方式你自己选,但要注意,nbformat.write 默认不会保留文件里乱七八糟的空白和顺序,有时候这反而是好事,因为保存后的文件更干净。
实际操作中,我见过有人直接修改 JSON 源码然后导致文件损坏,比如给 cell 少补了一个逗号、把 "cell_type": "code" 写错成 "type": "code"。如果只是临时改改,用 Python 操作 nb.cells[i].source = "xxx" 再写回,远比手改 JSON 安全。
3.2 常见批量操作:清理输出、替换关键词、统计使用情况
真实项目里你很少会只处理一个 notebook,更多时候是要批量操作几十个。我挑几个高频需求来演示。
第一个需求:清理所有 notebook 的输出。这在做版本管理时特别有用,因为输出内容经常制造大量无效 diff。用 nbformat 可以这样:
python复制import nbformat
from pathlib import Path
for path in Path(".").rglob("*.ipynb"):
nb = nbformat.read(path, as_version=4)
for cell in nb.cells:
if cell.cell_type == "code":
cell.outputs = []
cell.execution_count = None
nbformat.write(nb, path, version=4)
第二个需求:批量替换代码里的某个旧包名。比如你打算把项目里的 pandas 读取逻辑从旧接口换成新接口,一个个打开浏览器改太累,直接在文本层面处理就行:
python复制import nbformat
nb = nbformat.read("analysis.ipynb", as_version=4)
for cell in nb.cells:
if cell.cell_type == "code":
cell.source = cell.source.replace("df.append(", "df.concat(")
nbformat.write(nb, "analysis_new.ipynb", version=4)
注意这里有个细节:因为 source 可能是列表,也可能是字符串,最稳妥的办法是先把 "".join(cell.source) 变成标准字符串,替换完再赋值回去。nbformat 库在对象模式下会把 source 处理成字符串,但你不确定会不会遇到旧格式或者手写的异常文件时,我还是建议你写一个 normalize 函数,保证所有 cell 的 source 都是字符串再操作。
第三个需求:统计整个目录里 notebook 的总代码行数、Markdown 行数、输出数量。这听起来像 PM 才会关心的数据,但我在排查“某个 notebook 为什么这么慢”时也经常用,用来判断是不是有 cell 被无意识地反复执行,或者输出里藏了大量图片:
python复制import nbformat
from pathlib import Path
for path in Path(".").rglob("*.ipynb"):
nb = nbformat.read(path, as_version=4)
code_lines = 0
md_lines = 0
output_blocks = 0
for cell in nb.cells:
if cell.cell_type == "code":
code_lines += len(cell.source.splitlines())
output_blocks += len(cell.outputs)
elif cell.cell_type == "markdown":
md_lines += len(cell.source.splitlines())
print(path, "code:", code_lines, "md:", md_lines, "outputs:", output_blocks)
3.3 从 .ipynb 里提取可复用代码
还有一种很常见的诉求:notebook 里的逻辑写完了,想提成一个 .py 模块。最简单的方式是用 Jupyter 自带的 nbconvert:
bash复制jupyter nbconvert --to script analysis.ipynb
执行完会在同目录生成一个 analysis.py。这个脚本会把所有代码 cell 的源码按顺序拼接起来,Markdown cell 会变成注释,输出内容会被丢掉。但要注意,这种方式生成的脚本是顺序执行的,没有函数封装、没有 if __name__ == "__main__" 保护,直接 import 它可能会执行一堆初始化代码。
你如果希望提取的结果能作为模块被调用,建议先规划好 notebook 的结构,把可复用的函数集中在某些 cell 里,然后用代码按 cell 提取,而不是纯粹用 nbconvert 一锅端。比如这样:
python复制import nbformat
nb = nbformat.read("library.ipynb", as_version=4)
extracted = []
for cell in nb.cells:
if cell.cell_type == "code" and cell.source.strip():
extracted.append(cell.source)
with open("extracted_lib.py", "w", encoding="utf-8") as f:
f.write("\n\n".join(extracted))
这种方法的缺点是会丢掉执行顺序和中间变量之间的依赖逻辑。提取完以后,你还是需要手动整理一下 import 顺序、函数依赖。我的经验是,把 notebook 当作“草稿纸”,把 .py 当作“成品”,每次草稿定稿后都做一次清理,而不是在 notebook 里无止境地堆代码。
4. .ipynb 文件的版本控制与协作难题
4.1 为什么 Git diff 看起来像天书
只要一个团队用多个 .ipynb 协作,几乎都会遇到同一个问题:别人提交的 PR 里,明明只改了一个 cell,Git 却显示几百行增删。原因在于,.ipynb 保存的是包含输出、执行计数、元数据的完整 JSON,而不是你眼睛看到的那几行源码。
尤其是 Jupyter 每次执行后保存的 metadata 里可能包含执行时间、状态信息,这些信息在每次保存时都会变化。另外,如果你在浏览器里滚动鼠标、切换内核,某些前端插件也可能偷偷修改 metadata。换句话说,哪怕你一个字符都没改,只是打开文件再关闭,Git 都可能提示 diff。
面对这个问题,最原始的办法是靠团队约定:每次提交前清理输出。这样做确实有效,但纯靠自觉不现实。更稳妥的方式是用工具强行规范化。
4.2 推荐方案:Jupytext 的“双书模式”
我在实际项目里最常推荐的是 Jupytext。它不是一个文件格式的替代品,而是一个“配对器”。你可以同时保留一份 .ipynb 和一份 .py(或者 .md)配对文件。Jupyter 打开时读 .ipynb,Git 追踪时看 .py,两边内容自动同步。
安装并使用非常简单:
bash复制pip install jupytext
jupytext --to py analysis.ipynb
它会生成一个 analysis.py,默认情况下用 # %% 分隔 cell,里面也会有 # %% [markdown] 这样的标记来区分 Markdown cell。这个 .py 文件可以直接被 Git 跟踪,diff 看起来就是普通的文本差异。
如果你想让 Jupyter 每次保存时自动同步 .py 文件,可以在 Jupyter 配置里加上:
code复制c.ContentsManager.default_jupytext_formats = "ipynb,py"
c.ContentsManager.preferred_jupytext_formats_save = "ipynb,py"
用了这个模式以后,team 里审代码的人终于可以只用普通文件 diff 工具,而不需要打开浏览器。不过要知道,一对文件里的 .py 是“源”,.ipynb 是“渲染结果”,当你手动改了 .py 再打开 notebook,Jupytext 会提示你是否要 reload 到 notebook。
4.3 提交前清理输出的替代做法
如果你不想引入 Jupytext,那就得在 git 层面做一点手脚。可以在项目根目录加一个 pre-commit 钩子,用 nbformat 把所有改动的 .ipynb 清理输出后格式化,这个在多人协作里非常实用。
一个最小可用的 pre-commit 配置是:
yaml复制repos:
- repo: local
hooks:
- id: jupyter-clean
name: jupyter-clean
entry: jupyter nbconvert --ClearOutputPreprocessor.enabled=True --ClearMetadataPreprocessor.enabled=True --inplace
files: \.ipynb$
types: [file]
用这个 hook 以后,每次提交,但凡有 notebook 包含输出内容,都会被自动清掉。优点是一劳永逸,缺点是提交后本地 notebook 的输出也没了,你得重新运行。要解决这个矛盾,也有一个成熟方案:用 nbstripout 结合 Git 的 filter 机制,它会偷偷在提交时清理输出,但本地文件保持原样。它的安装方式是一行命令:
bash复制nbstripout --install
装完之后,你的 Git 会在处理 .ipynb 时自动过滤掉输出和元数据噪音。不过这个方案对团队里每个成员都需要做一次配置,从协作角度看,存在一定的学习成本。
5. 高频率踩坑现场:常见问题排查手册
5.1 文件打不开,提示 invalid notebook
这是我在新手阶段遇到最多的问题。绝大多数情况都是手动编辑 JSON 时破坏了格式,或者两个编辑器同时在写同一个文件,导致 JSON 不完整。
排查时先不要急着用 Jupyter 硬开,直接用 Python 看错误信息:
python复制import nbformat
try:
nbformat.read("broken.ipynb", as_version=4)
except Exception as e:
print(type(e).__name__, e)
如果错误信息指向 Expecting ',' delimiter 或 Unterminated string,基本可以确定是 JSON 语法错误。你可以在解密后用 json.load 定位:
python复制import json
with open("broken.ipynb", "r", encoding="utf-8") as f:
data = json.load(f)
如果 json.load 报错,它通常会提示具体的行号和列号,即使你不熟悉 JSON,也知道去哪一行找问题。常见原因包括:复制粘贴 Markdown 时带了不可见字符、在 image 的 base64 字符串里误删了字符、多个进程同时保存互相覆盖。
另外要留个心眼:如果文件非常大,超过几十 MB,不要期待 Jupyter 能秒开。这可能是 notebook 内部有大量 base64 图片输出。此时不要执着于用图形界面抢救,直接用代码把输出清空,往往就能恢复打开:
python复制import nbformat
nb = nbformat.read("large.ipynb", as_version=4)
for cell in nb.cells:
if cell.cell_type == "code":
cell.outputs = []
nbformat.write(nb, "large_cleaned.ipynb", version=4)
5.2 输出图片导致文件异常肥大
只要你在 notebook 里画过 matplotlib 图,并且没有把图片压缩或清除输出,文件体积就会迅速膨胀。一张高分辨率的 PNG 图片可能有几百 KB 到几 MB,保存成 base64 后还会再膨胀约 33%。
如果你需要长期保留图表,但不希望 notebook 文件变成“图片仓库”,有两条路:
- 用
plt.savefig("./figs/xxx.png")把图片保存到外部目录,再在 cell 里用IPython.display.Image引用外部文件路径,这样 notebook 文件里只存一个路径字符串。 - 用
OutputAreas里的清理功能定期清理图片输出,只保留核心文本结果。
需要注意的是,在版本控制里即使只执行一次绘图 cell,也会让 .ipynb 的 diff 变得非常大。所以如果你和同事共用仓库,我强烈建议约定:所有画图的 notebook,提交前把图片输出清掉或者降低图片 DPI。
5.3 cell 执行顺序错乱,结果对不上
Notebook 的 cell 是可以乱序执行的,这也是它比脚本灵活的原因。但灵活性带来一个坑:你本地的变量状态可能跟 cell 的视觉顺序不一致。你在第一个 cell 里定义了 x = 1,又跑到第五个 cell 里 x = 2,最后回到第二个 cell 打印 x,结果非常反直觉。
这类问题不是文件结构问题,而是你的“执行状态”被持久化了。排查时点到 Kernel -> Restart & Run All 看是否能复现,如果重跑全流程一切正常,那大概率是之前执行顺序导致的脏状态。
如果你已经确定是执行顺序的问题,但不知道是哪个 cell 动了变量,可以在 notebook 里临时插入一段“状态检查代码”,比如打印当前关键变量的名字和值,把顺序打乱的情况定位出来。
5.4 编码问题导致的中文乱码
.ipynb 文件内部是 UTF-8 编码。如果你用某些文本编辑器打开,或者不小心在 Windows 下用 GBK 编码另存,就会出现中文乱码,甚至导致 JSON 解析失败。
处理方式很简单:统一用 UTF-8。特别是在 Windows 环境里,用编辑器修改 .ipynb 时,别选“使用系统编码”,一定要选 UTF-8。如果你要写脚本读取文件,建议显式加上:
python复制with open("chinese_nb.ipynb", "r", encoding="utf-8") as f:
nb = nbformat.read(f, as_version=4)
在用 nbconvert 转换成 HTML 或 PDF 时,如果出现中文乱码,也可能是字体问题,但文件本身反而是正常的,不要搞混。
5.5 metadata 和依赖环境的隐患
团队协作时,如果某个人用的内核版本比较新,保存的 notebook 会写进去一些本地环境依赖信息。别人拿到这个文件后,如果环境不一致,Jupyter 会弹出“内核未找到”的提示,但这不代表文件损坏。你可以在 notebook 元数据里手动修改 kernelspec,比如统一改成 python3,这样至少保证大家都不会遇到危险的自动回退。
另外,有些第三方扩展会把自定义字段写进 cell 或文件 metadata。虽然不会立刻导致打不开,但在某些旧版本解析器上可能导致警告。如果排查问题很久无果,可以尝试把无关 metadata 删掉,只保留 language_info 和 kernelspec,我见过某些奇怪的渲染卡顿就是这样解决的。
6. 提升效率的进阶玩法:把 .ipynb 用得更顺手
6.1 自己写一个 notebook 摘要生成器
对很多数据团队来说,几十个 notebook 意味着几十个实验记录。人工一个个翻很累,我习惯用脚本生成摘要。大致思路就是读取所有 notebook,提取 cell 类型统计、第一个 Markdown 标题、是否存在特定关键词、有没有报错输出等。下面是一个浓缩版的例子:
python复制import nbformat
from pathlib import Path
import json
summary = []
for path in Path("notebooks").rglob("*.ipynb"):
nb = nbformat.read(path, as_version=4)
info = {
"path": str(path),
"cells": len(nb.cells),
"code_cells": 0,
"markdown_cells": 0,
"has_error": False,
"first_md": ""
}
for cell in nb.cells:
if cell.cell_type == "code":
info["code_cells"] += 1
for output in cell.get("outputs", []):
if output.get("output_type") == "error":
info["has_error"] = True
elif cell.cell_type == "markdown":
info["markdown_cells"] += 1
if not info["first_md"]:
first_line = cell.source.strip().split("\n")[0]
info["first_md"] = first_line[:80]
summary.append(info)
print(json.dumps(summary, ensure_ascii=False, indent=2))
这个东西看着简单,实际用的时候能帮你快速回答很多问题:哪些 notebook 已经报错?哪些 notebook 缺少说明文档?哪些 notebook 代码量巨大需要拆分?比在浏览器里一个个找快一个数量级。
6.2 用 papermill 参数化 notebook,变成批处理工具
如果你想保留 notebook 的交互可视化能力,又要让它能批量跑不同参数,那就值得了解一下 papermill。它可以直接从命令行或 Python 里传入参数并执行 notebook:
python复制import papermill as pm
pm.execute_notebook(
"template.ipynb",
"output.ipynb",
parameters={"dataset": "sales_2024.csv", "threshold": 0.8}
)
这样你的 notebook 就不再是一份只能手动点击的草稿,而是一个带参数的模板。配合调度平台,每天定时跑数据实验,再把结果输出成报告。这个玩法的前提是你得先在 notebook 某个 cell 里写上 # Parameters 标记,papermill 会在该位置插入变量。
这个场景的核心其实是,.ipynb 不仅能被人类阅读,还能被程序控制。理解了它的 JSON 本质后,你会发现一切自动化都只是对 JSON 的读写和执行。
6.3 把 notebook 转成演示文稿或网页报告
最后提一个被低估的能力:nbconvert 不仅支持转 .py,还能转 .html、.pdf、.slides。比如早上开周会前,把 notebook 转成一个可分享的 HTML 报告:
bash复制jupyter nbconvert --to html analysis.ipynb
生成的文件是自包含的 HTML,图片都通过 base64 内嵌在文件里,发给同事直接浏览器打开就能看,不需要安装 Python 环境。你如果要给客户做展示,还能用 --to slides 生成一个基于 reveal.js 的幻灯片,虽然排版细节还需要微调,但基础框架已经足够让人眼前一亮。
我在实际工作中,通常会把“模型实验记录.ipynb”作为日常探索的载体,到了需要汇报时,再根据听众身份选择导出成 HTML 或者 PDF。这样做既保留了过程记录,又不让交付变得笨重。
7. 关于 .ipynb 文件,我最终想说的几点实在话
有些人觉得用 notebook 写代码是“不专业”的表现,也有些人觉得 .py 脚本老土,我认为这两种观点都极端了。在我这几年处理过的项目里,最舒服的工作流是:探索阶段用 notebook,因为你能一边写代码一边记录想法,每一步都有可视化反馈;一旦逻辑稳定,立刻把核心逻辑抽进 .py 模块,notebook 只负责调用和展示。这样 .ipynb 就成为了项目的“驾驶舱”,而不是“发动机”。
给刚接触这个文件格式的朋友一个起点建议:先别急着学各种高级功能,找个时间用 Python 把你自己常用的某个 notebook 读一遍,逐个 cell 检查 cell_type、source、outputs 的结构。这个过程只需要半小时,但对理解 Jupyter 的工作方式帮助极大。
最后再分享一个小习惯:我会定期备份整个 notebooks 目录,同时把所有 notebook 统一通过 nbformat 做一次清理和规范化。因为文件格式虽然灵活,但如果没有纪律,几十个 notebook 很快就会变得难以维护。自己写一个目录级的清理脚本并不难,难的是坚持在每次交付前跑一遍。你真能坚持下去,后面省下来的时间绝对远大于编写脚本的投入。
