
上周帮一个做数据分析的朋友看他的笔记本他抱怨说“我明明建了虚拟环境pip 装了一堆包Jupyter Notebook 里 import 还是找不到”。打开一看他的 Notebook 是在 base 环境里启动的跑的也是 base 的解释器跟他那个虚拟环境一点关系都没有。这不是他一个人的问题——Anaconda 装好之后顺手打开 Jupyter 就开始写代码是很多人的默认路径而虚拟环境和 Jupyter 内核之间那条连接从来没有人主动替你做。这篇东西就是把这条链路讲透Anaconda 虚拟环境怎么建、环境目录和包缓存怎么从系统盘挪走、ipykernel 这类内核是怎么注册进 Jupyter 下拉列表的、nb_conda_kernels 自动发现和手动注册该怎么选以及内核装了却用不了时按什么顺序排查。前半段偏配置后半段偏排错跟着敲一遍机器上这套 Anaconda 虚拟环境加 Jupyter 内核配置基本就固化了。1. 为什么我不在 base 环境里写代码1.1 base 环境被搞脏的三条典型路径第一条路径最普遍装完 Anaconda直接pip install pandas numpy matplotlib全进了 base。过两个月再想装另一个项目需要的旧版本 pandaspip会告诉你“已满足依赖”然后你的老项目就崩了。第二条路径是 conda 和 pip 混用同一个环境conda 记录里写着 numpy 1.24pip 又把 numpy 覆盖成 1.26conda list显示的和实际 import 到的完全对不上。第三条是升级 base 环境里的 jupyter 相关包顺手把 pyzmq、jupyter_client 也升了结果内核挂不上。这三条路径的共同后果是base 环境变得不可复现。你不知道当前这个 base 到底是哪几个包的哪个版本在起作用出了 bug 也没法通过重建环境来验证。base 在我的用法里只有两个职责——提供 conda 命令本身和作为启动 Jupyter 的“调度台”业务代码一行都不在里面跑。1.2 环境、解释器、内核是三件不同的事这是理解整篇内容的地基。环境是 Anaconda 管理的一套隔离的包目录和解释器解释器是环境里那个具体的python.exe或python内核是 Jupyter 启动时真正 fork 出去的那个进程它由一份kernel.json描述里面写死了用哪个解释器、用什么参数启动。关键点在于Jupyter 启动时用的是它自己所在环境的 Python跟你在终端里conda activate到哪个环境毫无关系。终端里的激活状态只影响你在命令行敲python时指向谁Jupyter 是另一套进程它不认识你的终端状态。很多人卡在这一步好几年以为“激活了就该生效”。所以正确的做法是Jupyter 装在哪个环境都行我习惯装在 base但每个虚拟环境都要单独注册一个内核让 Jupyter 的下拉菜单里出现它的名字。两边解耦各自独立。1.3 conda 环境和 venv 的取舍我自己的判断标准很简单。涉及科学计算、需要 CUDA、需要非 Python 的二进制依赖如 MKL、GDAL、ffmpeg——用 conda 建环境因为 conda 会把 C 库一起装好pip 装这些东西经常编译到怀疑人生。纯 Web 开发、纯 Python 依赖、部署目标是容器——用 venv 更轻快而且部署时不会有 conda 那套目录结构带来的额外体积。顺带提一句 uv 这条路线。uv 建环境很快uv venv出来的.venv目录结构和 venv 一样注册内核时直接用.venv/bin/python -m ipykernel install --user --namexxx就行原理完全一致。区别只是创建环境那一步谁来做内核注册这一环没人能绕开。1.4 一个反直觉的结论我见过不少人为了“让 Jupyter 用上虚拟环境”把 Jupyter 装进每一个虚拟环境里。这个做法不算错但每个环境都塞一份 jupyter、notebook、jupyterlab、nbconvert一个环境就多出几百 MB而且版本各不相同改配置要改好几份。更省事的做法是反过来Jupyter 只装一份内核注册多份。改配置、升级 Jupyter、装扩展都只在一处动。这套思路在切换到下面要讲的 nb_conda_kernels 时尤其明显。2. 把环境和包缓存从默认位置挪走2.1 .condarc 里三个必须改的字段Anaconda 默认把所有环境建在安装目录下的envs里包缓存躺在pkgs里。装在系统盘的用户用不了半年 C 盘就红了尤其 conda 那个缓存目录删一次能腾出十几 GB 是常事。我的做法是装完 Anaconda 第一件事就改配置把环境和缓存都指到大容量数据盘。配置文件位置Windows 是C:\Users\你的用户名\.condarcLinux 和 macOS 是~/.condarc。这个文件初始不存在用命令行生成最稳妥省得手写出格式错误conda config --add envs_dirs D:\conda_envs conda config --add pkgs_dirs D:\conda_pkgs conda config --set show_channel_urls yesenvs_dirs是列表可以加多个路径conda 会按顺序查找和创建环境。注意顺序后加的会排在前面先用conda config --show envs_dirs确认一下哪个在最上面因为它决定了conda create -n xxx默认往哪写。改完配置后已经存在的环境不会自动迁移要么手动搬目录要么删了重建。手动搬环境在 conda 里是件很脏的事因为环境内很多脚本里写死了绝对路径我的建议一向是重建。2.2 镜像源怎么配才不踩坑国内直连默认源下载速度经常是几十 KB 每秒配镜像能省下大量等待时间。网上流传的配置版本很多下面这份是我这边长期在用的编辑~/.condarc写入channels: - defaults show_channel_urls: true default_channels: - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/msys2 custom_channels: conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud pytorch: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud配完执行conda clean -i清掉索引缓存再conda config --show channels确认生效。这里最容易踩的坑是镜像源里没有你要的包比如某些比较新的包只有 conda-forge 官方源有镜像同步有延迟。表现是PackagesNotFoundError但你上官网一搜明明有。这时候临时指定源就行conda install -c conda-forge 包名不用大改配置。pip 那一边也要顺手配一下不然 conda 装不上的包走 pip 还是慢pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple pip config set global.trusted-host pypi.tuna.tsinghua.edu.cn2.3 创建、激活、导出、删除的完整命令链把下面这套记熟日常够用。创建环境时明确指定 Python 版本这是环境可复现的基础conda create -n ds-py311 python3.11 -y conda activate ds-py311激活失败提示CommandNotFoundError说明 shell 没做过初始化。Linux/macOS 执行conda init bash或zshWindows 直接在开始菜单里用 Anaconda Prompt 就行别在普通 cmd 里硬试。改完conda init要重开终端才生效这一点经常被忽略。导出环境是多人协作的必备操作但直接conda env export会把平台相关的 build 号也写进去换台机器经常装不上。我的习惯是用--no-builds再手动把文件末尾那行prefix:删掉conda env export --no-builds environment.yml # 手动删除 environment.yml 里的 prefix 行 conda env create -f environment.yml如果只想复现你手动装过的那几个包用--from-history更干净缺点是 conda 有时会挑一个更新的版本需要自己再按environment.yml里的版本号手工锁一遍。删除环境前先确认自己不在那个环境里否则 Windows 上会报文件占用conda deactivate conda env remove -n ds-py311 -y再补一个conda clean -a把无用的包缓存和索引清掉这一步在大规模折腾之后能腾出可观的空间。3. 内核注册从虚拟环境到 Jupyter 列表的那条链路3.1 ipykernel 是被谁调用的Jupyter 前端浏览器里那个界面不执行代码它只是把代码通过 ZeroMQ 发给一个独立的内核进程内核执行完再把结果回传。Python 场景下这个内核进程就是ipykernel提供的ipykernel_launcher模块。所以一个环境要能被 Jupyter 使用必须满足两个条件环境里装了ipykernel提供可执行的入口以及在 Jupyter 能扫到的地方有一份kernel.json告诉 Jupyter 用哪个 Python 启动它。少任何一个下拉列表里都不会出现这个环境或者出现了但启动报错。安装 ipykernel 时我倾向用 conda 而不是 pip尤其环境里已经有 numpy、scipy 的时候走 conda 能保证这些依赖的二进制兼容性conda activate ds-py311 conda install ipykernel -y3.2 一条命令注册内核以及 kernel.json 里写了什么环境装好 ipykernel 后在该环境激活状态下执行python -m ipykernel install --user --name ds-py311 --display-name Python (ds-py311)三个参数逐个说清楚。--user表示写到当前用户的内核目录而不是 Anaconda 安装目录下的全局位置好处是不需要管理员权限且不污染安装目录。--name是内核的内部标识后面删除内核、别的工具引用它都靠这个名字不要用中文和空格。--display-name才是 Jupyter 下拉菜单里显示的名字可以随便起我一般带上 Python 版本号方便一眼区分。执行完用jupyter kernelspec list查看会打出所有已注册内核及其对应的目录。找到刚注册的那个进去看kernel.json{ argv: [ D:\\conda_envs\\ds-py311\\python.exe, -m, ipykernel_launcher, -f, {connection_file} ], display_name: Python (ds-py311), language: python }这段 JSON 就是全部秘密。argv第一项是解释器的绝对路径Jupyter 启动内核就是执行这条命令-f {connection_file}里的占位符会被替换成一个临时 JSON 文件路径里面存着这次会话的端口和密钥内核读它才知道往哪回传结果。理解了这一点很多问题就自明了如果这个绝对路径失效了内核必然启动失败如果这个解释器里删掉了 ipykernel启动一样会失败。反过来你完全可以手动复制一份kernel.json把路径改掉做出一个指向任意 Python 解释器的内核。3.3 内核目录在哪多用户机器上要留意什么内核目录分三级优先级从高到低大致是当前环境下的share/jupyter/kernels、用户级的 kernels 目录、系统级目录。用户级目录的位置各平台不同平台用户级内核目录Windows%APPDATA%\jupyter\kernelsLinux~/.local/share/jupyter/kernelsmacOS~/Library/Jupyter/kernels多用户服务器上有个常见现象A 用户注册的内核B 用户看不到因为内核写在各自的用户目录里。这本身是合理的隔离但如果想让大家共用一套就得把内核注册到共享位置或者干脆每人自己注册自己的。我在团队内网机器上的做法是每人在自己的 home 下注册避免互相覆盖和权限纠纷。删除不再用的内核直接对着 name 删就行比手动删目录安全jupyter kernelspec remove ds-py311这个操作只删内核定义不会删掉虚拟环境本身很多人以为删内核就把环境删了其实两回事。4. nb_conda_kernels 自动发现与手动注册的路线选择4.1 自动发现是怎么触发的nb_conda_kernels的思路是不手动注册让 Jupyter 启动时自己去扫 conda 的环境列表符合条件的自动加进内核菜单。装上之后新增环境不用再执行注册命令省事。但它的触发条件比较严格缺一个都不работа:第一它必须装在运行 Jupyter 的那个环境里我是装在 base第二被扫描的目标环境里必须有ipykernel第三conda 的环境列表能被正确读取也就是conda env list输出正常。三点都满足重启 Jupyter 后菜单里就会出现Python [conda env:ds-py311]这种格式的条目。安装方式conda activate base conda install nb_conda_kernels -y装完必须完全重启 Jupyter 服务光刷新浏览器页面没用因为内核列表是服务启动时扫描生成的。4.2 两条路线怎么选对比项手动注册nb_conda_kernels新增环境每个环境执行一次注册命令装好 ipykernel 后自动出现内核名控制完全自定义固定格式不易美化依赖只需 ipykernel需额外装插件且插件与 jupyter 版本有兼容关系环境很多时命令繁琐但可控明显省事出问题时的可读性kernel.json一眼看懂多一层扫描逻辑排查绕我自己的取舍是个人机器上环境不超过五六个用手动注册因为可控、透明出问题看kernel.json就够了。给不太熟悉命令行的同事配机器用插件版因为他后面自己建环境的时候不用回来找我。这两种方式可以共存不冲突。共存时菜单里会同时出现Python (ds-py311)和Python [conda env:ds-py311]两条看着乱删掉其中一个即可。4.3 顺序装反导致的空列表这是我踩过最典型的一次坑。当时我在一个全新的虚拟环境里先装了nb_conda_kernels再去 base 里装ipykernel结果内核列表始终是空的折腾了半小时。原因在于插件扫描的是“运行 Jupyter 的环境”和“目标环境”两个集合装错位置的那个环境既不是运行环境也没有出现在正确的位置。正确顺序是先在 base 装插件再逐个给虚拟环境装ipykernel。判断方法很简单conda list -n base | grep nb_conda确认插件在 baseconda list -n ds-py311 | grep ipykernel确认目标环境有内核包。还有一种隐藏情况是环境里有ipykernel但版本过旧和当前 jupyter_client 的协议对不上插件扫描到了却启动不了。这种就升级 ipykernel别硬扛。5. 排错实录内核装了却用不了5.1 单元格执行后光标变星号一直没反应现象是代码单元格里的星号[*]一直转不报错也不出结果重启内核也没用。这个问题的排查我一般按下面顺序走先看终端里 Jupyter 服务的日志输出通常会有一行内核启动失败的堆栈比界面上的信息多得多。再确认kernel.json里那个解释器路径是否真实存在环境被删过、目录被搬过都会导致路径失效。如果路径没问题检查目标环境里的 pyzmq 版本。这是最常见的一类原因pip 装某个包时顺带升级了 pyzmq版本和 jupyter_client 不匹配内核进程起来了但握不上手。第 3 条的处理办法是在出问题的那个环境里把相关包对齐而不是在整个 base 里瞎升级。可以先看版本再统一重装conda activate ds-py311 conda list | grep -i zmq conda install pyzmq jupyter_client ipykernel -y实测下来绝大多数“代码没反应”都能被这三步覆盖。剩下的少数情况是环境本身被 pip 和 conda 混装搞坏了那就用conda list --revisions回滚或者干脆重建环境——重建往往比继续排错快。注意在出问题的环境里执行pip install --upgrade pyzmq有时候会把问题搞得更复杂因为 pip 装的版本可能和 conda 记录的依赖冲突。优先用 conda 装。5.2 菜单里有名字一点就报内核挂掉这种通常是内核能启动、但启动后立刻退出。看服务端日志如果提示No module named ipykernel_launcher说明那个环境压根没装 ipykernel或者装了又被谁卸掉了。重新conda install ipykernel再注册一次内核即可。另一个可能是kernel.json被人手动改过JSON 格式坏了比如多了一个逗号Jupyter 解析失败。这种情况用jupyter kernelspec list一般会直接报错把那个内核目录删掉重新注册最省事。还有一种不太常见但很迷惑人的环境里的 Python 版本和系统位数不匹配比如 32 位的旧环境。这种只能重建。5.3 打不开界面、弹不出浏览器、端口被占jupyter notebook敲下去没反应先看终端有没有打出http://127.0.0.1:8888/?token...这一行。有说明服务正常起了浏览器没弹出而已手动把地址复制到浏览器即可。想彻底不弹浏览器加参数jupyter notebook --no-browser --port 8889端口被占用的报错很直白Address already in use换端口就行。想看看是谁占着Windows 上用netstat -ano | findstr 8888找到 PID 再对应进程Linux 上lsof -i:8888。界面要求输 token 而你不知道 token 是什么两个办法一是从终端那行?tokenxxx里复制二是启动时直接关掉jupyter notebook --no-browser --ServerApp.token --ServerApp.password注意关掉 token 意味着同局域网内任何人都能访问你正在运行的服务。只在完全可信的个人开发机上这么做别在公司内网随意关。新版 Jupyter 把配置项从NotebookApp迁移到了ServerApp网上很多老教程还在写--NotebookApp.token在新版本里会提示参数已废弃。看到这类警告直接换成ServerApp前缀即可。5.4 换了机器所有内核全部失效这个现象几乎必然发生因为kernel.json里是绝对路径。把环境从 A 机器拷到 B 机器路径变了内核定义还是旧的自然起不来。处理方式是到新机器上重新注册一遍jupyter kernelspec list jupyter kernelspec remove 旧名字 conda activate 新环境 python -m ipykernel install --user --name新名字 --display-name Python (新名字)如果是想把整个环境连同包一起搬到离线机器上conda-pack这条路比拷贝目录可靠得多conda pack -n ds-py311 -o ds-py311.tar.gz到目标机器上解压到某个目录执行source bin/activateWindows 下对应Scripts\activate.bat就能用。用这种打包方式搬过去的环境记得在新机器上重新注册一次内核别指望旧路径还能对上。6. 让这套环境顺手起来的几个配置6.1 Notebook 还是 Lab我现在的选择早期我一直用经典 Notebook界面简单直接。用久了发现三个痛点多文件切换麻烦、没有内置大纲、编辑体验偏弱。JupyterLab 在这三件事上明显更好尤其是左侧那个大纲面板长笔记本里跳转很快。现在的习惯是一次性分析、给同事发结果用 Notebook写较长、结构复杂、需要反复维护的项目用 Lab。两个可以装在同一个环境里互不影响启动命令分别是jupyter notebook和jupyter lab。如果两个版本装混乱了比如 Lab 4 和旧版 notebook 混在一起导致菜单异常最稳的做法是在 base 环境里统一升级conda activate base conda install -c conda-forge jupyterlab notebook -y6.2 补全、目录与导出这几件事代码自动补全这块JupyterLab 4 和 Notebook 7 已经自带基础补全但只有变量名和已导入模块的补全想要更完整的提示和跳转需要额外装conda install -c conda-forge jupyterlab-lsp python-lsp-server -y装完重启服务会在状态栏看到 LSP 的标识。如果只装jupyterlab-lsp不装服务端是没效果的这个坑挺常见。Markdown 目录生成我不推荐手写锚点因为 Jupyter 渲染中文标题时锚点规则不稳定。用 Lab 左侧大纲面板最省事非要在 Notebook 里做目录装jupyter-contrib-nbextensions里的 Table of Contents 扩展或者在文档开头写一组指向 Markdown 单元格的相对链接配合固定的英文锚点名。导出静态文件很适合交付装了 nbconvert 直接命令行转换jupyter nbconvert --to html report.ipynb jupyter nbconvert --to markdown report.ipynb要连着输出一起带上注意先执行完整个笔记本jupyter nbconvert --to html --execute report.ipynb。这一步在 CI 里跑自动化报表时特别有用。6.3 和 PyCharm、VSCode 共用一个环境这套配置的好处在这里体现出来了环境本身的目录结构是通用的IDE 认的是解释器路径Jupyter 认的是内核定义两边指向同一个环境装包不用装两份。PyCharm 里是File - Settings - Project - Python Interpreter - Add Interpreter - Conda Environment - 选择已有的环境解释器路径填具体环境下的python.exe。VSCode 里按CtrlShiftP选Python: Select Interpreter从列表里挑或者手动输入路径。VSCode 里跑 Jupyter 有个额外好处它内置了 notebook 编辑器可以不用起独立的 Jupyter 服务直接选内核就能跑。这时候如果能选到你的环境说明内核注册这一步做对了。拷给别人做演示时把笔记本转成 HTML 或者用 VSCode 插件导出比让对方配环境快得多。最后分享一个我自己的小习惯每建一个新环境安装完 ipykernel 后立刻执行一条组合命令确认状态——conda activate 新环境 python -c import ipykernel; print(ipykernel.__version__) jupyter kernelspec list三件事一次看清环境能不能激活、ipykernel 在不在、内核有没有被 Jupyter 识别。养成这个动作之后我基本没再遇到过“明明装了却找不到”的情况。