Jupyter Notebook网页端执行模型与内核管理实战 Jupyter Notebook 网页使用这几个字看起来是把一个本地工具搬到浏览器里点点鼠标的事。真正用上一两个月就会发现麻烦全在细节里内核莫名其妙卡死、变量和输出对不上号、换个目录启动就找不到数据文件、同一份 notebook 发给同事人家一跑全是报错。这些问题跟写代码的水平关系不大跟你有没有把网页这套执行模型吃透关系很大。我把 notebook 当成日常主力工作台用了挺长时间从最早在本地弹窗里写脚本到后来把它挂在实验室的机器上用浏览器远程连着跑几小时的训练任务中间踩过的坑基本覆盖了新手会遇到的绝大多数情况。这篇文章不打算复述官方文档里那些点这里、点那里的说明我更想讲清楚三件事网页端背后到底是谁在干活、每一个操作选择背后的取舍是什么、出问题时该按什么顺序排查。内容面向的读者范围我尽量放宽完全没用过、只知道它能写 Python 的人看完能顺利跑起来并且避开最常见的坑已经在用、但一直把它当能分块运行的记事本的人看完应该能把这套工具的效率再压榨一层。文中涉及的启动方式、配置项、命令都经过实际验证版本上以较新的 Notebook 7 为主涉及旧版本差异的地方我会单独标出来。1. 网页端到底在跑什么先把执行模型说透1.1 浏览器只是前台真正干活的是内核很多人第一次打开 notebook会下意识觉得代码是在网页里跑的。这个直觉是错的而且是后面一堆困惑的根源。浏览器在这里承担的角色更像一个终端加一个富文本编辑器它把你在单元格里敲的文字打包成一段消息发出去对面有个叫**内核kernel**的独立进程接收、执行、再把结果回传浏览器负责把结果渲染成表格、图像或者一段报错。这个分离结构解释了很多现象。比如你把浏览器标签页关掉内核并不会立刻死它还在后台算你那段跑了十分钟的循环比如你刷新页面变量还在因为内核没重启再比如你换了台电脑打开同一个地址看到的还是同一份运行状态。我之前就干过一件蠢事以为关掉标签页就等于停止任务结果第二天早上发现机器负载还是满的因为那个算了一整夜的内核一直在跑。理解这一层之后重启内核这个动作的含义也就清楚了它不是刷新页面而是把那个干活的进程杀掉再起一个新的所有变量、所有导入的模块、所有打开的文件句柄全部清零。这也是为什么我养成一个习惯交付 notebook 之前一定执行一次重启并全部运行确认从头到尾能跑通。提示浏览器只是显示层内核才是执行层。任何代码好像不对的怀疑第一步都应该是确认当前连的是哪个内核、这个内核是从什么时候开始跑的。1.2 为什么这种形态值得保留而不是直接用普通脚本说实话凡是需要长期维护、需要被别人 import 的逻辑我都会老老实实写成.py文件。notebook 的价值不在写代码而在探索。数据长什么样、某个字段有没有脏值、这个模型在这批样本上表现如何这类问题没法提前设计好流程你得边看边改。分块执行让改一行、看一次结果的成本降到几乎为零这个反馈速度是脚本加打印调试换不来的。第二个价值是叙事。单元格可以随时切成 Markdown把我为什么这么处理这一步的假设是什么写在代码正上方。半年后回看自己的分析过程那些注释救过我很多次。相比一堆散落的脚本文件一份结构清晰的 notebook 本身就是一份可执行的技术笔记。第三个价值是结果的富媒体呈现。表格、折线图、图片、公式、交互控件都能直接嵌在代码下方不需要另开窗口。给别人演示的时候滚动一遍就能讲完全过程这个体验确实比开终端加看图窗口要顺。代价也很明确执行顺序不受约束状态会残留diff 难看得要命。这三个问题在后面的章节我会逐个说应对办法。1.3 三类用法三种配置思路同样是网页使用用法差别很大配置策略也完全不同。我大致分成三类使用场景典型配置关键诉求本机一次性探索默认启动本地回环地址启动快不折腾固定项目长期使用独立虚拟环境 自定义工作目录环境隔离路径稳定挂在服务器上远程连着用指定绑定地址 密码鉴权 进程守护稳定、安全、断电不掉第一种最简单pip install完直接就开。第二种需要在项目目录里建虚拟环境然后把内核注册进去否则你今天装的包明天换个项目就冲突了。第三种要考虑的事情最多进程得能长期活着鉴权得做好端口得固定不然每次连都要去翻日志找地址。下面两节会把后两种的具体做法拆开讲。2. 启动与环境第一次打开之前该做的决定2.1 三条安装路径怎么选不后悔装 notebook 大致有三条路我不推荐哪个火用哪个而是看你后续要不要管理多个环境。第一条是直接用 pip 装进当前 Python 环境。命令很短pip install notebook。优点是干净、没有额外东西缺点是如果你机器上有多个项目早晚会遇到装了个包把另一个项目的依赖顶掉了的惨案。我个人只在容器或者一次性环境里这么干。第二条是用 conda 系的包管理器建独立环境。这是我在自己机器上的默认做法每个项目一个环境notebook 装在环境里互相不干扰。大致流程是先建环境再装包然后把环境注册成内核。这个注册内核的步骤是新手最容易漏的一步漏了就会出现我在终端里明明装了这个包为什么 notebook 里 import 失败这种经典问题。第三条是用集成发行版装完自带一堆常用库。好处是开箱即用坏处是自带的东西太多、版本你未必想要而且一旦和 pip 混用依赖关系容易乱成一锅粥。我给完全零基础的人推荐这条给准备长期用的人推荐第二条。# 独立环境 内核注册我常用的组合 conda create -n myproj python3.11 conda activate myproj pip install notebook pandas matplotlib python -m ipykernel install --user --name myproj --display-name Python (myproj)最后那行命令的作用是把当前环境登记到 Jupyter 的内核列表里--display-name是你在网页里看到的菜单项名字。写清楚一点很重要我见过有人注册了七八个环境名字全是Python 3选内核全靠猜。2.2 第一次启动那一串 token 是怎么来的在终端里敲jupyter notebook回车之后你会看到几行日志最后一行通常长这样http://127.0.0.1:8888/?token一长串字符。浏览器一般会自动弹出来如果没弹把这一整行粘进去就行注意 token 要连参数一起带只敲地址会要求你手动粘贴。这个 token 是一次性访问凭证机制很简单服务在本地监听一个端口任何请求必须带对 token 才放行防止你机器上的其他程序随便读你的文件。它每次启动都会变所以别存书签存了下次也是失效。如果嫌每次复制 token 麻烦可以设一个固定密码jupyter notebook password按提示输两遍会在你的用户配置目录下生成一个存着哈希值的文件。之后启动就直接输密码网页上会记住一段时间。注意密码文件是哈希存储的忘了没法找回只能重新执行一次上面的命令覆盖。设密码这一点在把服务挂到局域网里时几乎是必须的别图省事留 token。启动日志里还有几个信息值得看一眼Serving notebooks from local directory表示当前的工作根目录The Jupyter Notebook is running at后面是访问地址0 active kernels表示内核还没起。工作根目录这一条尤其重要因为它决定了你在网页文件列表里能看到什么、open(data.csv)这种相对路径从哪里开始找。2.3 配置文件把每次都要敲的参数固化下来每次都带一堆命令行参数不是长久之计。notebook 支持把常用配置写进文件生成方式是jupyter notebook --generate-config它会告诉你配置文件落在哪里通常在用户主目录下的.jupyter文件夹里。这里有一个版本差异必须提醒较新的 Notebook 7 底层换成了新的服务框架配置项的类名前缀从NotebookApp变成了ServerApp。如果你照着几年前的教程改了c.NotebookApp.notebook_dir却没生效八成就是这个原因。判断方法很简单打开配置文件翻一下看里面注释掉的示例用的是哪个前缀跟着它写就行。我常改的几项# 默认工作目录指向我的项目根目录省得每次 cd c.ServerApp.notebook_dir /home/me/work # 固定端口避免端口被占时自动跳到别的端口导致书签失效 c.ServerApp.port 8888 c.ServerApp.port_retries 0 # 不自动弹浏览器我在远程机器上根本不需要 c.ServerApp.open_browser False # 允许访问的地址按需设置 c.ServerApp.ip 127.0.0.1port_retries 0这一项是我吃了亏才加的。默认情况下端口被占会自动往后找结果是你以为还在 8888实际跑在 8889 上浏览器打开一片空白排查半天。设成 0 之后端口冲突会直接报错反而好定位。2.4 挂到局域网或服务器上鉴权是底线把服务跑在自己电脑上只给自己看安全性基本不用操心。但只要监听地址从本地回环改成对外的网卡地址性质就完全变了——这个服务的权限边界是能访问的人可以读写你账户下有权限的所有文件没有任何沙箱隔离。所以我只有两个建议第一一定要设密码第二能用端口转发就不要直接暴露端口。如果是在同一局域网内用把监听地址设成局域网地址、配好密码通常够用。如果是要跨网络连一台机器我一般的做法是只让它监听本地回环然后从自己的终端做一个端口转发把远端的端口映射到本地浏览器里访问本地地址效果一样暴露面小得多。进程要长期活着的话别用裸终端跑关掉会话就没了。用系统自带的服务管理方式或者终端复用工具挂住具体选哪个看你那台机器的习惯原则只有一条能开机自启、能看日志、挂了能自动拉起。3. 界面拆解与单元格操作用得顺不顺全在这3.1 主界面五个区域各管什么打开之后第一眼是文件列表也就是所谓的仪表盘。这个页面很多人直接跳过其实几个按钮挺有用右上角可以新建 notebook、新建终端、新建文本文件文件列表里勾选文件之后会出现移动、重命名、删除的操作栏顶部的 Running 标签能列出当前所有活着的内核和终端跑了一堆忘了关的时候从这里统一清理最方便。点进一个 notebook 之后界面大致分五块最上面是标题和文件操作往下是菜单栏再往下是工具栏保存、加行、剪切、运行、单元格类型、内核状态中间是单元格区域底部是内核状态指示。工具栏那排小图标我建议至少记住三个保存、运行、单元格类型下拉框剩下的大多用快捷键更快。右上角那个圆点值得单独说。空心表示内核空闲实心表示忙。一个跑了两分钟还没变空心的单元格说明确实在算别急着点重启但如果它一直是实心且日志里毫无动静那可能是真卡了这时候重启内核比干等着强。3.2 两种模式、三种运行方式单元格有两种状态编辑模式和命令模式。编辑模式就是光标在里面、能打字边框一般是绿色命令模式是选中了这个单元格但不能输入边框蓝色。这两个模式的区分是 Jupyter 所有快捷键体系的基础不搞清楚这一点按什么键都像在抽奖。切换方式命令模式按Enter进编辑编辑模式按Esc回命令。我早期最常犯的错就是以为自己在命令模式按了dd想删单元格结果只是在代码里打出了两个 d。运行单元格有三种方式区别在于运行之后光标去哪Ctrl Enter运行停在原地。适合反复调试同一个单元格。Shift Enter运行跳到下一格。适合从头往下推流程。Alt Enter运行并在下方插入一个新格。适合从零开始写。这三种我一天要用几百次尤其是Shift Enter和Alt Enter的组合基本能覆盖写代码的全部节奏。3.3 Markdown 单元格别把 notebook 写成流水账切单元格类型的方式是命令模式下按Y变代码、按M变 Markdown或者用工具栏下拉框。这一步看起来无关紧要但它决定了你半年后还能不能看懂这份文件。我给自己定的规矩是每一个逻辑段落前面必须有一个 Markdown 单元格写清楚这段要解决什么问题、输入是什么、为什么选这个方法。这不是为了好看是因为 notebook 的执行顺序可以被任意打乱如果没有文字锚点你回看的时候根本分不清哪段代码对应哪次分析。Markdown 单元格支持标准语法加公式标题、列表、表格、加粗、代码块都能用公式用美元符号包裹。写公式这一点在写分析报告时特别省事不用另外开文档工具排版。另外Shift Enter渲染之后双击可以回到编辑状态这个操作很多人不知道总以为渲染完就改不了了。3.4 执行顺序陷阱Out 编号会骗人这是 notebook 最大的坑没有之一。假设你在第 1 格定义了x 10第 2 格打印x然后你回去把第 1 格改成x 20但没有重新运行再运行第 2 格输出还是 10。等你在第 2 格上方插入新代码时编号可能变成In [7]和In [3]顺序彻底乱了。更隐蔽的情况是删除。你删掉了某一格但它定义过的变量还在内核里活着后面的代码照样能跑。等你把这份文件发给别人人家从头运行第一个格子就报NameError。我见过最惨的一次是某个分析脚本在本地跑出来的结论和同事复现的不一致查了大半天最后发现是因为我本地内核里残留了一个手工改过的中间变量而文件里根本没这行。从那以后我给自己定了一条硬规矩任何要交出去、要发出去的 notebook发之前必须重启内核并全部运行一遍。菜单里这个功能是现成的一次点击几十秒的事能省掉后面几小时的扯皮。判断顺序是否正常有个简单的自查方法从上往下看In [n]的编号如果不是从 1 开始依次递增就说明中途有乱序执行。全部重跑一遍编号就整齐了。4. 快捷键与魔法命令把重复动作压到一次按键4.1 我每天真正在用的那几个键快捷键列表很长但真正常用的就那么十来个。我按使用频率排了一下快捷键作用使用场景Shift Enter运行并下移顺序推进流程Ctrl Enter运行停在原地反复调同一段Esc/Enter切命令/编辑模式一切操作的前提A/B上方/下方插入单元格补代码DD删除当前单元格清理废格Z撤销删除手滑救星Y/M转代码 / 转 Markdown排版Shift M合并选中单元格收拢碎片Ctrl Shift -在光标处拆分单元格把一大坨拆开Shift Tab查看函数签名忘了参数怎么传Shift Tab这一条我要单独夸一下。光标停在函数名上按一下会浮出参数说明按两下展开更详细的文档按三下直接变成完整帮助。这比开浏览器查文档快十倍而且不用离开当前上下文。刚学某个库的时候我用这个键的频率比运行键还高。Ctrl Shift -拆分也很实用。写着写着发现一个单元格太长把光标放到要断开的位置按一下干净利落地分成两格比复制粘贴省事。4.2 行魔法与单元魔法省掉一堆样板代码以单个百分号开头的是行魔法作用于一行两个百分号开头的是单元魔法作用于整个单元格。下面几个是我几乎每个 notebook 都会用到的。# 计时单次执行耗时 %time result heavy_function(data) # 多次执行取统计跑基准测试用这个 %timeit -n 10 -r 3 sorted(random_list) # 整个单元格计时 %%time model.fit(X_train, y_train) # 自动重载外部模块改了 .py 文件不用重启内核 %load_ext autoreload %autoreload 2 # 直接把代码存成文件 %%writefile helper.py def normalize(x): return (x - x.mean()) / x.std()%autoreload 2这个组合我强烈建议写进每个 notebook 的第二个单元格。默认情况下你在别的文件里改了函数notebook 里 import 进来的还是旧版本必须重启内核才生效——重启意味着所有变量丢失之前算的十分钟白费。开了自动重载之后改完文件下一次调用就是新版本开发体验完全不一样。%timeit和%time的区别要说清楚前者会自动多次运行取平均值适合测很短的操作几微秒到几毫秒级别后者只跑一次适合测耗时较长的函数。用反了要么浪费时间要么结果噪声大到没法看。4.3 跟系统和文件打交道不用切窗口在单元格里用感叹号开头可以直接执行系统命令输出会原样显示在下面。这个功能方便到有点危险因为它没有任何保护。!ls -lh data/ !wc -l train.csv files !ls *.csv print(files)最后那种写法是把命令结果接到 Python 变量里返回的是一个字符串列表。做批量文件处理时挺顺手但我一般只用来做只读操作涉及删除、移动这类有副作用的命令我还是老老实实开终端窗口至少那里有完整的提示和确认。装包也可以用魔法命令%pip install和%conda install。相比直接!pip install它的好处是把包装到当前内核所在的那个环境里而不是你启动 notebook 的那个 shell 环境里。这两个环境经常不是同一个尤其在你注册了多个内核之后搞混了就变成我明明装了但 import 不到的经典问题。心得判断包到底装没装不要看终端直接在下一个单元格里跑import 包名; print(包名.__file__)打印出来的路径能一眼看出它属于哪个环境。4.4 观测与调试的几个小工具%who列出当前所有变量名%whos多给类型和值%reset -f清空全部变量。写长流程时我习惯在关键节点跑一次%whos看看内存里堆了多少东西有没有该删没删的大对象。调试方面%pdb打开之后任何报错都会自动进入交互式调试器可以打印变量、单步执行。不过说实话在 notebook 里做复杂调试体验一般真要跟一个 bug 死磕我还是会把它拎到正经的调试器里去。notebook 的强项是探索不是debug。还有一个用得少的%prun做性能分析能看到每个函数调用的耗时占比。优化前先跑一次这个比凭感觉猜哪里慢要靠谱得多。5. 数据展示、绘图与大输出处理5.1 绘图两种模式别搞混画图这件事上配置不同会导致行为完全不一样这一条踩坑的人特别多。# 静态内联图图固定在输出区最稳定 %matplotlib inline # 交互模式图可以缩放平移但会占一个独立区域且容易崩 %matplotlib notebook # 新一代交互后端体验更好需要单独安装 %matplotlib widgetinline是默认也是我最常用的输出是静态图片随 notebook 一起保存导出 HTML 的时候也在。缺点是图多的时候文件会变得非常大而且不能缩放。notebook模式能让图支持缩放和动态更新适合看三维散点或者需要连续更新的场景。但它的问题是不稳定尤其是图多、数据大的时候经常出现图不刷新或者整个前端变卡。分辨率也有讲究。默认输出的图在高分屏上看着发虚可以设一下%config InlineBackend.figure_format retina输出会清晰很多代价是图片体积翻几倍。给文档配图的时候值得日常探索我就用默认的。5.2 表格显示默认只显示一截是故意的用表格库展示数据时默认只显示前几行和后几行中间用省略号代替。这不是 bug是防止一个几百万行的表直接把浏览器渲染卡死。想看更多就改配置import pandas as pd pd.set_option(display.max_rows, 200) pd.set_option(display.max_columns, 50) pd.set_option(display.width, 200) pd.set_option(display.precision, 4) pd.set_option(display.max_colwidth, 80)这几个参数我一般只在需要仔细看数据的时候临时开看完就改回去。原因很简单设得太大一个不小心display(df)就能让页面卡十几秒尤其是带长文本列的时候。还有个技巧是善用display()函数。一个单元格里只写变量名只会显示最后一个值写成display(a); display(b)就能依次显示多个做数据对比时很方便。5.3 输出太大前端卡死的头号原因notebook 文件本质上是个文本文件所有输出都序列化存进去了。这意味着如果你在循环里打印了十万行日志或者把一个几百兆的数组直接显示出来文件会瞬间膨胀到几十兆浏览器打开就卡保存要等半天。几个应对手段按我使用的频率排第一循环里别裸打印。要么加个计数器只打关键节点要么用%%capture把输出截住不显示%%capture cap for i in range(100000): result process(i) # 需要的时候再看 print(len(cap.stdout))第二大数组别直接print看形状、看统计量、看前几行就够了。真想全看写到文件里再用别的方式打开。第三已经产生的垃圾输出要清掉。菜单里有清空当前单元格输出和清空全部输出的选项。命令行也能做导出之前跑一遍文件能小一个数量级jupyter nbconvert --ClearOutputPreprocessor.enabledTrue --inplace notebook.ipynb注意清空输出不可逆清之前先保存一份或者确认结果已经不需要了。我有过一次在演示前手滑清空全部输出只能从头重跑二十分钟。5.4 富媒体输出图片、进度条、超链接除了表格和图notebook 还能直接嵌其他东西。展示图片可以用专门的显示模块写报告时把关键截图贴进去比在外面维护一堆图片文件再引用要省心。跑长循环的时候加个进度条能显著减少焦虑。装个轻量的进度条库把循环对象包一层就行它会实时在输出区刷新跑完自动变成一行完成信息。我原来都是靠打印计数器估进度用了进度条之后再也回不去了。还有一个小细节单元格里的最后一行如果是个对象会调用它的展示方法。所以自定义类的时候实现一下对应的展示方法就能让它以 HTML 表格或者带格式的形式出现调试自定义数据结构时特别直观。6. 内核管理重启、清空、多环境6.1 什么时候该重启什么时候不该内核菜单里那几个选项含义差别很大很多人一直混着用操作做了什么什么时候用Restart杀进程重开变量全丢状态脏了、卡死了Restart Clear Output重开并清空所有输出交付前整理Restart Run All重开并从头跑一遍交付前必做Interrupt中断当前执行死循环、跑太久Clear Output只清输出减小文件体积Interrupt是被低估的一个。写了个死循环或者发现参数写错想中止按中断比等它跑完好得多。中断之后内核还在变量也还在只是当前那个单元格停了。但如果中断之后状态变得奇怪比如某些操作只做了一半那就别再接着跑了直接重启更干净。Restart Run All我前面提过这里再强调一次它的意义它是唯一能验证这份文件从头到尾自洽的方式。任何交付、任何发布、任何给别人看的 notebook这一步不能省。6.2 多环境内核和终端不是一个东西这一节可能是新手最容易困住的地方。你在终端里conda activate myenv然后启动 notebooknotebook 用的是myenv但如果你之后在另一个终端里激活了other_env再启动一个 notebook两个浏览器页面连的是两个不同的内核装包互不影响。更常见的情况是notebook 从桌面图标或者菜单直接启动它继承的是系统默认环境不是你以为的那个。于是就有了我 pip install 明明成功了import 还是报错。排查顺序应该是这样的先看网页右上角的内核名字再看import sys; print(sys.executable)打印的解释器路径最后确认这个路径和你装包的那个环境是不是同一个。三步下来问题基本都能定位。要新增内核用第 2 节里那条注册命令。要删除内核用jupyter kernelspec list看列表然后jupyter kernelspec remove 名字。我每隔一段时间会清理一次删掉那些早就不用的老旧环境不然新建 notebook 的时候下拉菜单长得能滚动半天。6.3 内存问题变量不删就一直占着内核是个常驻进程里面所有变量都不会自动回收。这点和跑脚本完全不一样——脚本跑完进程退出内存全还回去notebook 里你加载了一个几 G 的数据表哪怕后面再也不用了它还在那儿蹲着。几个处理习惯处理完大对象立刻del然后调一下垃圾回收模块虽然不保证立刻还给系统但至少标记了。用%whos定期看看谁在占地方尤其是那些名字叫df、df2、df_copy、temp的中间变量全是历史遗留。真要彻底清干净重启内核是唯一可靠的办法。另一个隐藏的内存杀手是图画得太多。每张图的对象、每个大数组的输出都会留在内核里。画了几十张图之后内存飙升是正常现象这时候清输出加重启能立刻恢复。7. 常见故障排查实录7.1 启动不起来先看端口和路径最常见的启动失败是端口被占。报错信息里会明确写地址已被使用。定位办法很直接查一下谁占着这个端口# Linux / macOS lsof -i :8888 # Windows netstat -ano | findstr 8888查出来之后要么杀掉那个进程要么换个端口。我前面说的port_retries 0在这里就派上用场了——它让问题立刻暴露而不是默默换个端口让你找不着。第二常见的是工作目录不存在。配置文件里写了个路径但那个目录被删了或者移动了启动会直接报错。这种错误信息通常很直白照着改就行。第三是命令找不到提示jupyter: command not found。这基本都是路径问题包装在某个环境里但那个环境的可执行目录没在当前 shell 的 PATH 里。解决办法是激活对应环境再启动或者干脆用python -m notebook这种写法让它跟着当前的 Python 走。7.2 内核连不上一直转圈或者自动重连页面能打开但每个单元格跑起来都是转圈然后断开重连提示内核似乎挂了。这种问题我遇到过几次原因各不相同。一种是内核进程真的崩了。最常见于内存耗尽被杀或者某个底层库崩了。表现是日志里能看到进程退出的记录。这种情况重启内核就行但要留意是不是数据太大下次还会崩。另一种是环境里的依赖坏了。比如你在 notebook 运行期间用 pip 升级了某个底层库正在跑的内核引用的还是旧版本的文件两边对不上就崩了。所以我在 notebook 开着的时候基本不动环境要装包就装完重启一次内核。还有一种是浏览器缓存的老页面在反复请求一个已经不存在的会话。表现是刷新就好过一会儿又断。处理办法是彻底关掉标签页重新从地址进别用历史记录里的旧链接。7.3 显示异常图不显示、中文变方块画了图但输出区一片空白只显示一行对象地址。这个问题九成是绘图后端没配好或者忘了在某处调用展示函数。先确认开头有没有设内联后端再看是不是在函数里画的图但没返回、也没显示。中文显示成方块是另一个高频问题本质是默认字体里没有中文字形。解决办法是显式指定一个系统里存在的中文字体并顺手把坐标轴的负号显示修一下否则负号也会变成方块。这段配置我一般放在 notebook 开头的绘图设置区一次配好后面所有图都正常。公式渲染不出来通常是 Markdown 语法写错了比如美元符号没配对或者前后多了空格。渲染失败的时候不会报错只会原样显示找起来比较费劲建议写完扫一眼。7.4 排查速查表把上面这些整理成一张表出问题时从上往下对照现象最可能的原因第一步动作命令找不到环境路径没进 PATH换python -m notebook试页面打不开端口被占 / 地址写错查端口占用看启动日志的完整地址要求 token 或密码鉴权正常行为从终端日志里复制带 token 的完整链接import 报错找不到模块内核环境 ≠ 装包环境打印解释器路径核对变量莫名不存在内核重启过 / 单元格顺序乱看In [n]编号全部重跑跑起来一直转圈内存耗尽 / 依赖冲突看日志是否有进程退出记录图不显示后端没设 / 没调用展示开头补上内联后端配置中文方块缺中文字体显式指定字体文件几十兆打不开输出太多清空输出导出前跑清理命令卡得动不了输出过大 / 图太多中断清输出重启内核8. 从个人工具走向团队可用8.1 导出与交付别让人家装环境才能看notebook 最尴尬的一点是它的原生格式需要环境才能打开。发给不看代码的人最好的方式是导出成静态网页滚动就能看图和表都在里面# 导出网页 jupyter nbconvert --to html report.ipynb # 导出 Markdown方便进文档系统 jupyter nbconvert --to markdown report.ipynb # 先从头执行一遍再导出确保结果是最新的 jupyter nbconvert --to html --execute report.ipynb--execute这个参数很关键。它会先把整份文件从头跑一遍再导出保证你看到的输出和代码是匹配的不会出现图上写的是旧数据这种事。当然前提是这份文件本身能从头跑通——又是那句话交付前先自己跑一遍。如果对方也是要跑代码的人那就直接发.ipynb文件但一定要在文件开头写清楚依赖版本或者干脆给个环境描述文件。我吃过太多次在我这儿好好的的亏最后发现是对方库的版本差了一个大版本号。8.2 和版本控制配合把输出剥掉再提交.ipynb本质是个 JSON 文件里面混着代码、输出、执行计数、图片的编码数据。直接提交到版本控制里diff 会变得完全没法看——改了一行代码diff 里可能全是 base64 的图片数据。处理办法有两种。一种是在提交前把输出清掉命令前面给过--ClearOutputPreprocessor.enabledTrue --inplace加个--inplace是原地改写。另一种是装个提交钩子工具在提交时自动剥掉输出仓库里只留代码。我个人倾向后者因为靠人记得每次手动清是不现实的早晚会漏。还有一个细节执行计数也会进文件导致同样的代码因为跑的次数不同而产生 diff。清理输出的时候一般会一并重置不用单独处理。8.3 什么时候该把它改写成脚本notebook 不适合做长期维护的工程代码这一点我态度很明确。判断标准大致是这几条中一条就该考虑抽成.py代码要被别的程序调用需要在无人值守的情况下定时跑逻辑分支复杂到需要写单元测试文件长到滚动条都嫌累。这几种情况下 notebook 的交互性反而成了负担你没法可靠地测它也没法干净地 import 它。我一般的做法是探索阶段全在 notebook 里等逻辑稳定了把核心函数抽到模块文件里notebook 只负责调用和展示。这样既保留了探索的灵活性又让核心逻辑可以被测试、被复用、被别人引用。抽出去之后notebook 里那个自动重载的开关就显得格外重要了——改模块文件notebook 里立刻生效不用重启。我个人在实际操作中的体会是notebook 最大的价值不是能分块跑代码而是它把思考过程和执行过程放在了同一个平面上。你能看到自己当时的假设、试错、修正全都在一页纸上。这个特性用好了它就不只是个工具而是你工作过程的存档。也正因为如此那些跑完就扔的临时格子最好及时清掉留下来的应该是对以后有用的东西——一份三个月后自己还能看懂、别人也能接手的工作记录。