深夜十一点半,你敲下jupyter notebook,终端很快吐出一行地址:http://localhost:8888/tree?token=...,然后光标一闪一闪地停在那儿。你等着浏览器自己蹦出来,等了十秒,屏幕纹丝不动;手动把地址粘进浏览器,又被告知"无法访问此网站"。这个场景我遇到过太多次,它和代码写错没有任何关系,纯粹是 Jupyter notebook 的启动链路在某一环断掉了。这篇文章就是把这个链路拆成几段,一段一段告诉你怎么定位、怎么修,顺带把 rpds 导入失败、单元格跑不动这些常被混在一起的问题一并说清楚。适合刚装好环境的新手,也适合那些已经用了几年、却每次出问题都靠"重装大法"的老用户。
1. 先判断"打不开"卡在哪一环,比盲目重装环境有用
大部分人一遇到 Jupyter notebook 打不开,第一反应是卸载重装 Anaconda,或者删掉虚拟环境重建。这个动作十分钟起步,而且大概率下一次还会犯。真正省时间的做法是先分清病因——"网页跳不过去"至少有四种完全不同的表现,每一种的修法都不一样。
1.1 四种表现,对应四类根因
下面这张表是我自己排错时用的第一道筛子,你对着现象看一眼,基本就能把范围缩到一两类:
| 你看到的现象 | 大概率根因 | 优先动作 |
|---|---|---|
| 终端有地址,浏览器完全没弹出来 | 自动打开浏览器的机制失效,或默认浏览器没配 | 手动复制地址打开,先确认服务本身是否正常 |
| 浏览器弹出来了,提示"无法访问此网站""拒绝连接" | 服务没真正起来,或地址里的端口不是服务实际监听的端口 | 回头看终端回显的端口号 |
| 页面打开,要求输入 token 或密码,输了还是进不去 | 地址里丢了 token 参数,或本地存在旧的鉴权配置 | 用jupyter notebook list取最新地址 |
| 页面白屏、一直转圈、控制台报资源加载失败 | 浏览器缓存污染,或前端静态资源与版本不匹配 | 换浏览器无痕模式试一次 |
为什么第一件事要看现象而不是看日志?因为 Jupyter 的报错信息经常是"下游"报错。比如端口被占用导致它静默换了端口,终端里可能只有一行很不起眼的提示,你要是不注意就会一直盯着浏览器折腾。
1.2 从终端回显里抠出四条关键信息
Jupyter notebook 启动时打印的那一小段文字,信息密度其实非常高,只是大多数人扫一眼就过去了。你要从里面拿到的有四样东西:
- 完整的 URL,注意它通常长这样:
http://127.0.0.1:8888/tree?token=3f8a...,端口和 token 都在里面; - 实际监听的端口,如果你没指定,它可能不是 8888;
- token 字符串,一长串十六进制字符,中间不要漏字符,它不是装饰;
- 启动目录,也就是它把哪个文件夹当成了根目录,目录权限不对也会出问题。
这里有个新手常踩的坑:token 从终端复制到浏览器时,如果终端做了自动换行,很容易只复制了前半段。表现就是页面能打开、token 校验失败,看起来像是"跳转不过去",其实是地址不完整。
1.3 一个反直觉的结论:先别急着删环境
我自己的经验是,九成以上的"Jupyter notebook 打不开"都是参数级问题,跟包是否装好关系不大。判断方法很简单:在终端里执行jupyter --version,如果能正常打印出 notebook、jupyter_core、ipykernel 各自的版本号,说明程序本身是完整的,问题出在启动参数或浏览器侧。
只有在下面这几种情况下,重装才是有意义的:命令完全找不到(jupyter: command not found)、版本号打印直接报错、或者接下来第 5 节会讲的 rpds 那类二进制依赖冲突。其余情况,重装只是把问题暂时藏起来了。
2. 把地址握在自己手里:主动接管启动流程
既然"自动跳转"是最脆弱的一环,那最省心的做法就是不让它跳。Jupyter 本来就提供了关闭自动打开浏览器的开关,只是很多人不知道,或者知道了懒得用。
2.1--no-browser的正确姿势
jupyter notebook --no-browser --port=8888 --ip=127.0.0.1这三个参数分开看,每一个都有明确意图:
--no-browser:告诉 Jupyter 别去调用系统默认浏览器。这一步能把"浏览器调用失败"这条错误分支彻底掐断,让服务的启动过程变得干净可控;--port=8888:把端口钉死。这样你就不需要猜它到底跑到哪儿去了;--ip=127.0.0.1:把监听地址钉死为本地回环地址,避免出现第 3 节要讲的 localhost 解析歧义。
为什么我强烈建议把端口写死而不是让它自动挑?因为一旦端口是动态的,你脑子里的"Jupyter 就是 8888"这个印象就会欺骗你,而实际服务已经跑到 8889 或 8890 上去了。
启动完后,终端会打印一行带 token 的完整地址,直接整行选中复制到浏览器地址栏。这里有个小技巧:在多数终端里双击那一行就能整行选中,比手动拖拽准确得多。
2.2 token 到底在干什么,为什么不能随手删
token 是 Jupyter 用来确认"访问这个服务的人是不是启动它的人"的凭据。因为 Jupyter notebook 本质上是一个跑在你机器上的小型 Web 服务,任何能访问这个地址的进程理论上都能执行代码,所以这道校验不能省。
但如果你是在自己的单机环境里,每次都要粘贴一长串 token 确实很烦。正规的替代方案是设置密码,而不是简单粗暴地把 token 清空:
jupyter notebook password执行后它会让你输入并确认密码,然后把哈希值写进配置文件。之后你再打开页面,输密码就行,不用再跟那串十六进制字符较劲。这比--NotebookApp.token=''安全得多——后者等于把门锁拆了,本机自用尚可,一旦是在共享的机器上,风险不小。
2.3 不同安装方式,命令入口不一样
这一步经常被忽略:同一条jupyter notebook,在不同环境里指向的可执行文件是不同的。你在 A 环境里装好了,在 B 环境里敲同样的命令,可能报"命令未找到",或者启动了一个完全不相干的环境。
| 安装方式 | 推荐启动方式 | 容易出问题的地方 |
|---|---|---|
| pip 全局安装 | jupyter notebook | 全局包冲突,多个版本互相覆盖 |
| venv 虚拟环境 | 先激活环境再执行 | 忘记激活,用的是全局的那个 |
| conda 环境 | conda activate 环境名后执行 | 环境激活了但没装 notebook 包 |
| conda base 环境 | python -m notebook | base 环境里 PATH 被其他工具改过 |
遇到">命令找不到"时,最直接的验证手段是python -m jupyter notebook,用当前 Python 解释器去调模块,能绕过 PATH 层面的干扰。如果你确实想知道命令来自哪里,which jupyter(Windows 上用where jupyter)会告诉你答案。
3. 端口与监听地址:被误判最多的两个原因
这一节讲的两个坑,症状都像"网页跳不过去",但根因跟浏览器没半点关系。我见过太多人在这两个点上反复折腾浏览器设置,最后发现改一条启动参数就好了。
3.1 端口被占用后的"静默换端口"
8888 是 Jupyter 的默认端口,也正因为它是默认值,特别容易被别的程序占用:另一个没关掉的 notebook 实例、某个本地开发服务器、甚至某些开发工具自带的预览服务,都可能占着这个位置。
一旦被占用,Jupyter 的行为是——它不报错,而是自动往后找一个空闲端口。终端里会出现类似port 8888 is already in use, trying another port的一行提示,然后继续启动。问题就在于这行字淹没在一堆输出里,你根本没注意到,然后依旧去打开localhost:8888,看到的当然是别人的服务或者干脆是错误页。
确认端口占用的命令按平台区分:
# Linux / macOS lsof -i :8888 ss -lntp | grep 8888 # Windows netstat -ano | findstr :8888拿到占用进程的 PID 之后,要么把它结束掉,要么换端口启动。我个人的习惯是直接把端口固定成 8890 这类不容易撞车的值,写进配置文件,从此不再纠结。
3.2 localhost 可能被解析成 ::1
这是最隐蔽的一个坑。localhost在部分系统的 hosts 配置里同时映射到 IPv6 的::1和 IPv4 的127.0.0.1,而浏览器的优先级可能倾向 IPv6。如果 Jupyter 只监听在127.0.0.1上,浏览器去连::1就会连接失败。
判断方法非常快:把地址栏里的localhost换成127.0.0.1,其他部分一个字都不改,回车。
- 换成
127.0.0.1能打开、localhost打不开:就是这个解析问题,解决方法有两个——启动时加--ip=127.0.0.1并用127.0.0.1访问,或者在配置文件里把监听地址设成localhost; - 两个都打不开:说明问题不在解析,回到上一节看端口。
很多人卡在这里,是因为他们测试时随手用了localhost,报错后又随手换成127.0.0.1发现好了,却以为是"随机好了一下",下次照旧踩。
3.3 安全软件和系统防火墙的拦截
还有个不常被提但确实存在的情况:本地的安全软件、系统防火墙的入站规则,可能把 Jupyter 启动的进程当成未知的网络服务给拦了。表现是服务在终端里看着一切正常,浏览器却一直转圈。
验证思路很朴素:临时关掉安全软件的网页防护,再试一次。如果能打开,说明是拦截导致的,然后去把对应的进程加进白名单,而不是长期关着防护。
注意:把监听地址设成
0.0.0.0会让同一网络内的其他设备也能访问这个服务,本机自用完全没必要这么干;如果确实需要,务必同时设置密码,别关掉鉴权。
4. 用配置文件把启动参数固化下来
每次启动都敲一长串参数,既不优雅也容易忘。Jupyter 提供了一份 Python 格式的配置文件,把常用参数写进去,以后一条命令搞定。
4.1 生成并找到配置文件
jupyter notebook --generate-config执行后会告诉你文件写在了哪里,通常是用户主目录下的.jupyter/jupyter_notebook_config.py。如果你不确定 Jupyter 到底在读哪些路径,用这个命令看全部:
jupyter --paths它会一次性列出配置目录、数据目录、运行时目录。这个命令在"配置改了没生效"的场景里特别好用——很多时候你改的是另一个环境下的配置文件。
4.2 关键字段逐个说明
打开配置文件,你会看到满屏被注释掉的选项,全部展开大概有几百行。真正需要动的其实只有几条:
# 监听地址,钉死为 IPv4 回环,避开 localhost 解析歧义 c.NotebookApp.ip = '127.0.0.1' # 固定端口,避免静默换端口 c.NotebookApp.port = 8890 # 关闭自动打开浏览器,把控制权交给自己 c.NotebookApp.open_browser = False # 启动时的默认工作目录,路径别带中文和空格 c.NotebookApp.notebook_dir = '/Users/yourname/work/notebooks'关于最后一条我要多说两句。notebook_dir的路径里如果包含中文、空格或特殊字符,在 Windows 上有一定概率导致启动异常或目录识别错误。稳妥做法是专门建一个纯英文、无空格的目录作为 notebook 的家。
另外,c.NotebookApp.port改了以后,你访问的地址也要跟着改,别再习惯性地点历史的书签——这属于自己给自己制造的"跳转失败"。
4.3 新旧版本的字段名已经不一样了
这是个容易让人抓狂的变化:Jupyter Notebook 7 和 JupyterLab 底层换成了 Jupyter Server,很多配置项的前缀从NotebookApp变成了ServerApp。你把参数写在旧名字下,新版本根本不理你,表现就是"配置改了但完全不生效"。
| 功能 | 旧前缀(Notebook 6 及以前) | 新前缀(Notebook 7 / JupyterLab) |
|---|---|---|
| 监听地址 | c.NotebookApp.ip | c.ServerApp.ip |
| 端口 | c.NotebookApp.port | c.ServerApp.port |
| 自动开浏览器 | c.NotebookApp.open_browser | c.ServerApp.open_browser |
| 根目录 | c.NotebookApp.notebook_dir | c.ServerApp.root_dir |
| 鉴权 token | c.NotebookApp.token | c.ServerApp.token |
最稳的办法是不猜:直接执行jupyter notebook --show-config,它会打印出当前实际生效的完整配置。看一眼输出里用的是哪个前缀,照着写就不会错。这个命令我是强烈推荐加到日常工具箱里的。
4.4 一条命令列出所有在跑的服务
jupyter notebook list这条命令会列出当前机器上所有正在运行的 notebook 服务,以及它们各自的完整访问地址(带 token)。当你忘了自己开了几个实例、不知道哪个端口对应哪个目录时,这是最快的答案。我把它当成"找回入口"的万能钥匙。
5. 依赖与内核层面的连带问题
前面四节解决的是"网页打不开",但实际排错时经常是几个问题纠缠在一起。这一节把两个高频的连带问题拆开讲。
5.1ImportError: DLL load failed while importing rpds
这个报错在 Windows 上出现频率很高,而且它会让 Jupyter 直接起不来,看起来像是"打不开网页",其实根本没走到启动那一步。
根因是rpds-py这个包的二进制扩展与当前 Python 解释器不匹配。它通常是被jsonschema间接拉进来的依赖,而较新的 Python 版本(比如 3.12 之后)在某些阶段缺少对应的预编译轮子,pip 就可能装到一个不兼容的版本,或者退化成源码编译但编译环境不完整。
处理顺序建议这样来:
- 先升级到最新版,让它拿到匹配当前解释器的轮子:
pip install --upgrade rpds-py jsonschema- 如果升级后还是同样的报错,强制只用二进制轮子安装,避免走源码编译路径:
pip install --only-binary :all: --force-reinstall rpds-py- 如果你用的是 conda 环境,走 conda 渠道往往更省事,因为它会整体检查二进制兼容性:
conda install -c conda-forge rpds-py- 以上都不行,考虑把 Python 版本回到 3.11 这类生态兼容性更成熟的版本。这不是"退步",在数据处理场景里,能用、稳定比版本号新不新重要得多。
有个判断细节:这个报错里如果还夹着_rpds或pydantic_core之类的名字,那大概率是同一批 Rust 扩展包的二进制问题,处理思路一样。
5.2 单元格执行代码没反应,跟跳转问题是两码事
另一个高频热词是"单元格执行代码没有任何反应"。它和网页打不开经常被混为一谈,但根因完全不同:这是内核层面的问题。
典型表现是左上角或右上角显示的内核状态一直是"空闲"或"正在连接",你按 Shift+Enter,前面的In [ ]就是不变数字。排查顺序:
- 先看内核名称。如果显示的是"无内核"或者一个已经不存在的环境,点菜单里的重启内核,或者换一个内核;
- 用
jupyter kernelspec list看看当前注册了哪些内核,路径是否指向真实存在的 Python; - 如果你在虚拟环境里装过
ipykernel又删过环境,kernelspec里会残留一条指向空路径的记录,这种"僵尸内核"点上去就是毫无反应,需要手动清理; - 偶尔是内核进程崩了但前端不知道,强制重启内核一般就好。
我踩过最冤的一次,是某个环境里的ipykernel版本和 notebook 主版本差得太多,表现为新建的 notebook 能打开、能编辑,但一执行就卡住。把ipykernel升到与 notebook 大致匹配的版本后恢复正常。
5.3 编辑器里连内核时,需要的不是网址
现在不少人习惯在编辑器里写代码、连 Jupyter 内核跑单元格。这种情况下的"连不上"和浏览器场景是两套逻辑:编辑器要的是内核连接文件,而不是那个http://开头的地址。你得先在终端把 notebook 服务或内核跑起来,编辑器插件会自动去扫描本机的运行实例。
如果你确实需要在编辑器里手动指定,那就去找运行时目录下名为kernel-xxxx.json的连接文件,里面记录了地址、端口和密钥。拿浏览器的地址去填这个位置,是连不上的——这是两个不同的对接协议。
6. 我的排查顺序清单与几个长期有效的习惯
把上面的内容压缩成一条可执行的行动线,遇到 Jupyter notebook 无法跳转网页时,照着走一遍,基本十分钟内能定位:
- 看终端回显:确认端口号和完整 URL,别凭印象假设是 8888;
- 手动复制地址:整行复制,确保 token 没被截断;
- 把 localhost 换成 127.0.0.1:一步就能验证是不是解析问题;
- 测端口占用:
lsof -i :8888或netstat -ano | findstr :8888; - 换端口重启:
jupyter notebook --no-browser --port=8890 --ip=127.0.0.1; - 查运行实例:
jupyter notebook list找回真实入口; - 看生效配置:
jupyter notebook --show-config,确认前缀写对了没有; - 还不行再查环境:
jupyter --version、jupyter kernelspec list,排查依赖和内核。
几个我自己长期保留的习惯,分享出来供参考:
- 端口固定为 8890,写进配置文件。这个端口撞车概率远低于 8888,而且一旦固定,脑子里就不会有"到底在哪个端口"的疑问;
- 永远带
--no-browser。让浏览器自己弹出来的那一瞬间看起来省事,但出问题时它会同时引入"是浏览器没被调用,还是服务没起来"这个额外的判断分支,排查成本反而更高; - 地址用
127.0.0.1而不是localhost。放弃一点输入的便利,换取行为的一致性,这笔账怎么算都划算; - 不用的实例随手关掉。Jupyter 是那种"开着不管也没事"的服务,但堆积起来之后,端口冲突和内核混乱的概率会显著上升;
- 配置文件里所有路径用纯英文无空格。这是最不起眼、却最能减少玄学问题的一条。
最后再补一个容易被忽略的细节:如果你是从旧版本升级上来的,老配置文件里的NotebookApp前缀不会自动迁移,新版本会静默忽略它们。升级之后第一次启动发现"配置全都不生效了",去看一眼配置文件里的前缀,往往比重新装一遍环境要快得多。