我从2016年第一次接触Jupyter Notebook,中间有过好几次“这玩意儿到底有什么用”的念头,但真正在做数据处理和机器学习实验之后,反而越来越依赖它。先讲一个特别日常的痛点:你用普通.py脚本做数据清洗,前面二十行负责加载CSV,中间几行处理缺失值,最后两行画图。画图方案改来改去,每次改完都得从头到尾重新跑一遍,光等待数据重新读入就快半分钟。而且如果加载之后还连着网络请求、预处理步骤,整个调试周期会被拉得很长。Jupyter想解决的,正是这个问题:把“写一段代码—运行—看见结果—再改”的反馈周期压缩到最小。这篇内容我会从它的核心机制讲起,再聊安装、目录、执行状态、复制粘贴这些实际使用里最容易摔跤的地方,最后说说为什么我依然建议你用Jupyter。不管你是刚装完Miniconda还不知道怎么启动,还是已经被“一个cell只输出最后一行”搞到困惑,又或者是启动时报错打不开,都可以在下面找到对应的思路。
1. 为什么Jupyter不是“笔记本版编辑器”:它把脚本执行方式彻底改了
1.1 普通脚本和Notebook的差别,一句话就能说清楚
很多人把Jupyter Notebook当成一个带富文本的代码编辑器,这是最大的误解。普通的.py文件,你的代码是一个整体,要么一口气从头跑到尾,要么就干脆不跑。中间想停下来看看某个变量,只能靠打日志、设断点,或者手动在代码里插一句print再重跑。Notebook不是这个逻辑,它把一个完整的任务拆成了多个带顺序的代码块,也就是“单元格”。你可以只执行第二个单元格,跳过第三个,回头再执行第一个,每个单元格把自己这段代码提交给同一个Python内核去执行。
真正关键的地方在于:内核是一直活着的。第一个单元格把一个DataFrame加载进内存,第二个单元格可以接着用,不需要再重新读一遍文件。如果一个文件要重复加载十次,普通脚本就重复读十次;在Notebook里,只需要读一次。这也是为什么很多数据岗位的人一但用顺了这个模式,就再也回不去那种“改一行、全量重跑”的方式。再加上每个单元格既能显示普通文本输出,也能直接渲染表格、图表和富文本内容,它天然就适合做探索性分析。
1.2 状态保留带来的工作流变化
状态保留把开发方式从“编译型思路”变成了“交互式思路”。比如你想观察一个清洗函数对数据的中间影响,你只需要在前面单元格里把原始数据准备好,然后反复修改后面一个单元格,单独执行它,就能立刻看到效果。数据不动,代码动;反馈时间极短。这在分析一堆结构复杂、来源又多的数据时,收益尤其明显。你在做特征工程时,经常需要试很多种转换方式,对比不同参数下的分布结果,这种边改边看的能力比任何花哨的调试器都来得直接。
我在实际项目里最常见的模式是:第一个单元格负责加载文件和依赖包,第二个单元格做全局配置,第三个单元格写核心预处理函数,后面几十个单元格全是验证和可视化。这样即使后面某个实验方向完全走不通,我也只需要回到第三个到第五个单元格重新组合,前面加载好的数据仍然在内存里。这种“数据不重载,代码可重试”的模式,适合做研究、做探索、写教学文档,但不适合直接当生产脚本。生产脚本要求确定性,要求入口清晰,要求异常时能快速重跑,这些反而是Notebook的短板。
1.3 但正因为状态可以“偷偷残留”,纪律更重要
Notebook给自由度,但也带来了一个副作用:单元格执行顺序可以被你任意打乱,变量残留问题就出现了。假设你先执行了一个包含df = df.dropna()的单元格,后面所有依赖df的单元格看到的都是删过缺失值的数据。如果你新建一个空白Notebook,按顺序一个个跑,结果没问题;可如果你反复跳着执行,某些旧变量就会“偷偷”留在内存里,让结果看起来正确,实际上依赖的是上一个实验的中间状态。这可能是Notebook被不少人批评“不可复现”的主要原因。
我的习惯是:每隔一段时间就用一次“Restart & Run All”,而不是继续在当前会话里来回补跑。只靠手动点执行,代码的正确性其实是建立在“内存状态正好符合我预期”这个脆弱假设上的。重新启动内核再全部跑一遍,才算真正验证了整套流程是可重复的。准确说,Notebook不是不能产出可靠结果,而是要主动用流程纪律去约束自由度。
2. 从“一个cell只输出最后一行”讲起,Notebook的显示规则与常见误区
2.1 自动显示只是最后一行表达式的值
很多新手在Notebook里写下面这种代码,发现只看到一个结果,前面几个计算全“消失”了。
1 + 1 2 + 2 3 + 3执行之后,页面上只出现6。这不叫文件出错,而是IPython内核的显示规则:一个单元格运行完后,如果最后一行是一个表达式,并且它返回的不是None,系统会把它的“代表形式”自动显示出来。前面的1 + 1和2 + 2虽然在内部都计算了,但因为没有语句要求打印,结果就静默消失了。这个规则和普通Python脚本里的“不调用print就不输出”是不太一样的,很容易让人误以为代码没执行。
当最后一行执行结果本身就是None时,也不会显示任何输出。例如这样:
df = load_data() df.columns.tolist() df.head()如果df.head()是最后一行,那个表会显示;如果后面还有一句print("done"),df.head()的结果就会被吞掉。原因还是同一个:不是print的表达式,默认不会自动回显。遇到这种情况,不要怀疑是数据没了,而要想一想这个值是不是真的被“显示”了。
2.2 想让中间的结果都出来,有两个方向
想同时看到多个中间结果,最简单的办法是显式写print。print(1 + 1)、print(2 + 2)、print(3 + 3),输出就会一行行出现。但print在打印DataFrame这类结构时,效果好,可排版上和Notebook本身的富文本呈现相比还是要弱一些。
我更推荐在需要展示复杂对象时使用IPython.display模块里的display函数。
from IPython.display import display a = 1 b = 2 print(a) display(b) display(a + b)display会走Notebook的富文本展示通道。同样的对象,print展示的是字符串形式,而display更接近Jupyter内核对外表现出的“原生渲染”效果。在同一个单元格里要展示多个图表或者多个DataFrame时,display几乎是标准做法。它不会像print那样把结构压成纯文本,保留的格式更完整。
2.3 图形输出、Matplotlib与多个figure的处理
Matplotlib在Notebook里的行为也经常让人迷惑。你可能会在某次运行时看到图,但在加了另一行代码之后图就不见了。最常见的原因是单元格最后一行不是绘图对象,而是别的表达式。例如:
import matplotlib.pyplot as plt import numpy as np %matplotlib inline fig, ax = plt.subplots() ax.plot(np.random.randn(50))如果最后一行是ax.plot(...),图会显示,但上方可能还会跟着一行[<matplotlib.lines.Line2D at 0x...>]文本。想在既显示图又不出现多余对象文本,比较好的做法是代码块末尾调用一次plt.show(),或者明确放在最后一个表达式的位置。想在一个单元格里并排展示两张图,不要指望两个plot会自动分栏,可以用display(fig)分别控制。
from IPython.display import display fig1, ax1 = plt.subplots() fig2, ax2 = plt.subplots() ax1.plot(...) ax2.plot(...) display(fig1) display(fig2)这个习惯很小,但能省下不少“明明画了图为什么没显示”的排查时间。
3. 环境准备思路:Miniconda、命令行启动,以及启动时“找不到指定程序”
3.1 先选Anaconda还是Miniconda
安装Jupyter之前,大部分人会先碰见一个选择:Anaconda还是Miniconda,或者直接用系统Python。Anaconda的好处是省心,装完就自带几百个常用科学计算库,Jupyter Notebook也默认配好了。坏处是体积大,装很多你用不上的包,而且包之间版本冲突时,重装代价更高。Miniconda只包含conda和Python核心,体积小,环境干净,要用什么再装什么,但初上手时你需要自己装Jupyter。
如果只是想在笔记本上快速体验数据处理,装Anaconda确实最省时间。如果你打算认真经营自己的分析环境,愿意花一次半小时的时间搞定基础配置,我会更倾向于Miniconda。数据分析和机器学习项目里,环境之间的隔离比“一步到位”重要得多。不同项目需要的numpy、pandas版本经常不一样,conda环境可以把它们隔开,避免一次升级把另一个项目搞崩。
3.2 miniconda装好后,怎么把notebook拉起来
Miniconda安装完成后,开始菜单里会有一个“Anaconda Prompt”或者“Miniforge Prompt”这类终端入口。打开后默认处于base环境,这时候输入jupyter notebook就能启动。出现一堆日志后,浏览器会自动打开http://localhost:8888/tree。如果页面没自动打开,就复制终端里类似http://127.0.0.1:8888/tree?token=...的地址到浏览器手动访问。
如果系统提示jupyter不是内部或外部命令,先确认是不是没有安装。可以执行:
conda install -c conda-forge notebook jupyterlab安装完成后再输入jupyter notebook。不要直奔“下载独立Jupyter安装包”这种思路,Jupyter本身是一组Python包,用conda管理最顺。另一个常见原因是你开了两个终端,一个在conda环境里,另一个是系统默认cmd。conda环境里装了jupyter,但cmd启动的是另一个Python环境里的jupyter入口,于是经常出现“Anaconda里能打开、cmd里打不开”的奇怪局面。
3.3 “启动时显示找不到指定的程序”排查链
“启动时显示找不到指定的程序”这类报错,在Windows电脑上并不少见,尤其是在此前正常使用过Anaconda或Miniconda,后来又重装过其他Python环境的机器上。它不一定代表Jupyter文件损坏,也可能是运行库或者路径映射出了问题。
我遇到这类报错时一般按顺序排查,而不是一上来就卸载重装:
| 症状 | 优先怀疑点 | 建议动作 |
|---|---|---|
| 加载时报“找不到指定的程序” | conda与系统Python路径被搞混 | 改用Anaconda Prompt启动,先conda activate,再jupyter notebook |
输入jupyter提示找不到命令 | jupyter包没装,或当前环境不对 | conda install -c conda-forge notebook jupyterlab |
输入python -m notebook能开,但直接输入jupyter不行 | PATH里的exe位置异常 | 执行where jupyter,如果能看到多个路径,保留一个 |
弹窗显示缺少某个.dll文件 | Windows运行库缺失,或被杀毒软件隔离 | 安装Visual C++ Redistributable,检查安全软件的隔离区 |
那种“Anaconda的jupyter打不开,突然用不了了”的情况,很多时候不是因为Jupyter本体坏了,而是某个底层依赖包版本被意外更新,或者安全软件把一些动态库隔离了。可以先在Anaconda Prompt里跑一句python -m notebook --version,如果这条命令能正常输出版本号,说明Python解释器还能找到notebook包,问题大概率出在.exe启动器本身;如果提示No module named notebook,那就说明环境里的notebook确实没装上或者已经损坏。比较稳妥的补救方式是重新安装核心包:
conda install notebook jupyterlab --force-reinstall我还会做一次“冷启动验证”:关掉所有终端,重新打开Anaconda Prompt,先确认命令行提示符前面有环境名字,再启动Jupyter。很多诡异问题在干净重启之后自然就消失了。
4. 让Notebook落在你想要的文件夹:工作目录、默认存储路径与网页端登录
4.1 命令行指定启动目录:最直接的方法
Jupyter启动起来之后,你在网页上看到的文件夹列表,直接对应着启动时的工作目录。想进入一个特定项目文件夹,最直接的方法是先切到那个目录,再启动服务。Windows上可以是:
cd D:\data_project jupyter notebook如果一开始就不想切目录,也可以直接通过参数指定:
jupyter notebook --notebook-dir=D:\data_project新版JupyterLab也支持类似写法,只是参数略有变化:
jupyter lab --ServerApp.root_dir=D:\data_project如果你发现浏览器打开之后看到的还是老目录,大概率是配置文件里已经有默认路径,而且配置文件的优先级淹没了你的临时参数。这时候可以先用一个临时端口启动,比如--port=8899,确认启动日志显示的root目录是不是你想要的,再决定是不是要清理配置文件。
4.2 生成配置文件并设置默认目录
要让Jupyter以后每次默认打开某个目录,可以先把配置文件生成出来。
jupyter notebook --generate-config生成的配置文件一般位于用户目录下的.jupyter\jupyter_notebook_config.py。Windows上的路径类似C:\Users\你的用户名\.jupyter\jupyter_notebook_config.py。编辑时找到和目录相关的两个常见配置项:
c.ServerApp.root_dir = 'D:/data_project' c.NotebookApp.notebook_dir = 'D:/data_project'不同版本里字段名有区别,新版JupyterLab推荐使用ServerApp.root_dir。我见过有人同时写两个字段,结果一个生效一个不生效,反而更容易迷惑。你要做的是先执行jupyter --paths,确认Jupyter实际加载的是哪个路径下的配置文件,然后只保留一份设置。另外路径里最好用正斜杠,Windows路径里的反斜杠在配置字符串里需要转义,容易写错。
4.3 “网页版登录入口”的token到底是什么
启动Jupyter时,终端里会出现一个带有token=的完整URL,例如http://127.0.0.1:8888/tree?token=123456...。这个token是临时身份凭证。你如果在浏览器里手动输入http://localhost:8888,页面会让你填写一个token或者密码,返回终端复制的完整带token地址是最快的登录方式。很多人把“网页版登录入口”理解成一套复杂系统,其实它只是Web服务自身的认证页面。
如果不想每次复制token,可以在配置文件里设置密码。先生成密码的哈希值:
jupyter notebook password运行后会提示输入两次密码,之后Jupyter会把哈希写入相关配置。设置完成后,再启动服务,登录页面输入你设置的密码就可以了。需要注意,这个登录页本身不提供任何加密传输,如果在一台公网机器上以0.0.0.0方式绑定所有网卡,等于把数据分析端口暴露到网络上,风险很高。我一般只在可信内网里使用远程访问,生产环境如果必须开放,也会在前方加一层反向代理做访问控制,而不是直接裸跑Jupyter服务。
4.4 在别的文件夹里创建notebook的几种做法
“怎么在别的文件夹创建Jupyter文件”也是一个高频问题。实际上原理很简单:你先要到目标目录,然后在那里创建一个新的notebook。最简单的方式是先在本地打开那个目录,然后执行jupyter notebook。比如你的数据都放在D:\study\python,那就在Anaconda Prompt里先cd D:\study\python,再启动。打开Dashboard之后,点击右上角的“New”,选择Python 3,新文件就会创建在当前工作目录下。
如果你不想重启服务,也可以在JupyterLab的左侧文件浏览器里,直接点击导航栏进入任意子文件夹,然后新建文件。旧版Notebook的Dashboard同样可以通过文件夹导航进入,点进去之后右上角仍然有New按钮。要注意的是,如果当前文件夹没有写入权限,新文件按钮会变灰。还有一种方式是把一个已有的notebook通过“Upload”上传到当前服务目录,这适合数据源已经在远程服务器或者另一台设备上的情况。反复操作几次之后你自然会理解:Jupyter网页里看到的目录,就是启动它的那台机器上的某个真实磁盘路径。
5. 长时间运行的cell怎么看执行进度,而不是对着一堆“In [*]”干等
5.1 In[*]到底代表什么,以及真正可用的状态提示
Notebook里每个单元格左侧显示的In [1]代表这个单元已经按顺序提交给内核执行过。In [*]则代表它正在执行、还没返回。如果你看到In [*]一直不消失,那就是这段代码正在运行,或者已经卡住了。JupyterLab的界面会同时有另一个提示:点击上方菜单的“运行”或查看底部状态栏,能看到“Kernel Busy”和上次执行耗时。旧版Notebook则通过工具栏上的圆圈是否实心来判断内核状态。
但这些东西只能告诉你“正在跑”,不能告诉你“跑到百分之几”。想要精细进度,得在代码层面自己提供信号。最基本的做法是循环里每隔一段时间打印一个计数:
import time n = 100 for i in range(n): time.sleep(0.05) if i % 10 == 0: print(f"已完成 {i + 1}/{n}")这种方式虽然土,但稳定、直观。只要输出流不是被缓冲得太厉害,你基本能判断当前走到哪里。
5.2 几个实用的进度输出方案
更体面的做法是使用tqdm。在notebook场景里,用tqdm.notebook,它会在单元格输出区渲染一个实时进度条,效果比终端里的文本进度条更清晰。
from tqdm.notebook import tqdm import time for i in tqdm(range(100)): time.sleep(0.05)如果tqdm没有安装,先执行conda install -c conda-forge tqdm。它会显示当前迭代数、总迭代数和运行时间,基本就能回答“Jupyter怎么看执行进度”。对于单个单元格,还可以使用%%time和%time。%%time放在单元格最上面,会统计整个单元格的运行时间;%time则放在单行语句前面,统计这一行的执行时间。
%%time result = [x * 2 for x in range(1000000)]如果一个大任务运行很久,进度条能让你知道它没死,但我更推荐的是提前把中间结果落盘。数据分析项目里经常出现“跑到80%才发现第20行有个字段名拼错了”的尴尬场景。前置几个关键结果到CSV或Pickle文件,比开着Notebook等一晚上更安全。
5.3 中断、重启与“关键结果先落盘”的保命习惯
长时间运行的时候,你肯定需要“中断”这个操作。工具栏上的停止按钮,对应Jupyter内核的“Interrupt”,它会向正在运行的代码发出中断信号,通常会在Python代码里触发KeyboardInterrupt,然后停止当前单元格。内核本身还活着,前面已经计算好的变量大部分还保留着。如果你遇到的是完全卡死,按停止也没反应,那就只能在“Kernel”菜单里选择“Restart”,它会重新启动内核,所有内存变量都会被清空。
这里有一个我踩过多次的坑:长时间训练模型或者大批量处理数据,最好不要把还没保存的结果只放在内存里。重启内核之后,前面单元格就算写得再完美,变量也不在了。我现在遇到长任务,第一步先把中间产物保存下来,例如用df.to_parquet('temp.parquet')或者pd.to_pickle。宁可多存几个临时文件,也不要赌“这次肯定不会崩”。
如果你想完整重跑一遍notebook,推荐“Restart & Run All”而不是手动一个个点击。这会清空所有残留变量,然后严格按照从上到下的顺序执行所有单元格。我每次在交付分析结论之前,都至少用它跑一遍,确保结果不是靠某个特定执行顺序凑出来的。
6. 浏览器里复制粘贴、键盘操作与其他容易翻车的日常细节
6.1 为什么有时候粘进去的代码会变形
复制粘贴在Jupyter里的体感,和普通文本编辑器不太一样。很多人第一次从网页或者其他文档里复制代码,粘进去之后发现#变成了标题格式,*好像也被吞了,字符串引号变成了奇奇怪怪的“智能引号”。出现这个问题的原因,大概率是你正在粘贴的是某个富文本来源,比如Word、公众号文章,或者带格式渲染的网页。JupyterLab默认会对剪贴板上带格式的内容做转换,于是纯文本代码被当成了一段富文本。
解决办法是把粘贴目标切换成纯文本。在JupyterLab里可以用编辑菜单里的“粘贴为纯文本”,或者直接使用浏览器的“无格式粘贴”快捷键,通常是Ctrl+Shift+V。还有一种办法是先把代码贴到系统自带的记事本里洗一遍,再复制出来粘进Notebook,虽然绕了一步,但能保证缩进和空白字符都不被额外处理。
如果是从Notebook内部复制单元格,记得先点击单元格左侧选中区域,然后在编辑模式下复制。在浏览器里直接选中代码字符进行复制,只会复制你选中的字符;但如果你需要复制一个完整的单元格,拖选不是最靠谱的方式。JupyterLab支持在命令模式下按C复制、V粘贴,但前提是你理解了它自带的剪贴板机制。更简单的方式是先用鼠标点选单元格左侧让它出现高亮边框,然后按C复制,再新建一个单元格按V粘贴。这段逻辑和普通文本编辑器不太一样,需要花几分钟适应。
6.2 快捷键养成,比敲“运行”两个字有用得多
Jupyter的快捷键虽然多,最核心的也就几个。编辑模式下按Shift+Enter执行当前单元格并跳转到下一个,按Ctrl+Enter执行但不跳转,按Alt+Enter执行并在下方插入一个新单元格。命令模式下按A在上方插入,B在下方插入,连续两次D删除当前单元格,Z撤销。记住这套组合之后,操作速度会明显快起来,因为你不再需要频繁移动鼠标去点击运行按钮。
如果你分不清自己处于编辑模式还是命令模式,窍门是看单元格边框颜色。命令模式下单元格边框是蓝色,编辑模式下是绿色,内部光标可见。很多复制粘贴问题,本质上就是当前处于命令模式,浏览器焦点被Jupyter接收,快捷键被它优先抢走了。遇到粘贴不生效,先按一下Enter进入编辑模式,再试一次。
6.3 跨文件复制、多选单元格和撤销删除
JupyterLab里可以一次选中多个单元格,方法是点击左侧行号区域并用Shift多选,然后统一执行、复制或者移动。这对从上到下处理一整套流程很有用。比如你想把前面几个数据清洗的单元格挪到另一个Notebook里复用,多选后复制,再到目标位置粘贴,能省掉很多重复劳动。
撤销删除也是一个容易忽略的小技巧。在命令模式下误按DD删掉了单元格,不要急着重新打开文件历史,按一下Z通常能恢复。对经常高强度编辑notebook的人来说,这个操作和“保存文件”一样重要。另外,JupyterLab本身不是强制的单标签页工作流,你可以打开多个Notebook,共享同一个内核。跨文件共享变量这种事虽然可以做,但我建议少用,因为文件之间的依赖一旦建立,单个文件的可读性和可复现性都会下降。
7. 说了这么多麻烦之后,为什么我还是建议你用Jupyter
7.1 适合它发挥的场景,恰好是大多数数据分析刚开始时的场景
如果你只是想写一个稳定运行的定时脚本,或者做一个对外发布的服务API,Jupyter肯定不是最优选择,甚至有点憋屈。但数据分析、机器学习实验、模型调参、写教学资料,这些场景里最重要的不是“代码能不能上线”,而是“我能不能快速看懂数据是什么样,特征怎么变,结果怎么来的”。Jupyter把代码、执行结果、图表、描述文字放在同一个文件里,观察者不需要像看代码仓库一样来回跳转,一眼就能看到当时的分析逻辑和最终输出,这对探索性工作来说是很大的优势。
我自己现在习惯是用Notebook做研究的“现场记录”。每次想验证一个假设,就把数据和代码放进一个轻微注释的单元格,运行完立刻看到结果;做完之后,再把涉及稳定的核心逻辑抽成.py模块,notebook本身变成可以回看的实验日志。后面写文档也好,跟同事解释结论也好,效果都很好。这种把代码和叙事结合的方式,恰恰是Jupyter最擅长的。
7.2 我的建议:先把notebook当“实验记录本”,再决定要不要改成产线脚本
如果你正打算开始用Jupyter,我的建议是不要把它当成一个必须马上把所有快捷键学完的工具。先把notebook当成一个带记忆的实验记录本:一个文件,多个单元格,按顺序执行,加必要的Markdown标题。等你在“交互式探索”里体会到了那种改一步看一步的感觉,再去整理自己的环境、目录和快捷键习惯。
我之前见过一个新手,因为“一个cell只输出最后一行”的问题差点卸载Jupyter,但后来发现只需要把想看的中间结果用display显示出来就好,这是显示规则的差异,不是软件坏了。很多类似困惑,本质上都来自“用传统脚本编辑器思维去套Notebook的执行模型”。反过来想,一个工具能让你用最短时间验证一个数据问题,还能顺手把图给画出来,这两点就足以构成“为什么建议你用Jupyter”的答案。后面真正需要细抠的,反而应该是如何管理好环境、目录和运行纪律,那才是长期使用里最值钱的经验。