昨天在那台 Ubuntu 服务器上装 markdown 和 pyyaml,pip3 install 直接给我脸色看:
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
一开始以为是 pip 版本太老,升了没用。其实这跟 pip 版本没关系,是 Debian/Ubuntu 从 23.04 开始搞的 PEP 668。
这报错到底在说什么
系统里有个标记文件:
$ ls -l /usr/lib/python3.12/EXTERNALLY-MANAGED
-rw-r--r-- 1 root root 2490 ... EXTERNALLY-MANAGED
有它在,pip 就认为"这个 Python 的包由系统 apt 管着,你别乱动"。理由不算没道理:apt 装的 python3-requests 和 pip 装的 requests 会互相覆盖,把 apt 自己的依赖搞崩,apt upgrade 时给你报一堆冲突。pip 干脆提前拒绝,省得你手动收拾。
四个方案,我实际怎么选的
方案一:venv,正道。 缺点是这台机器上 python3 -m venv 直接报 ensurepip is not available,因为系统没装 python3-venv 包。而且这台机 PEP 668 管得死,装它也得走 apt,得先 sudo。折腾。
sudo apt update && sudo apt install -y python3-venv
python3 -m venv ~/venv
source ~/venv/bin/activate
pip install markdown pyyaml
venv 目录随便放,激活后 which pip 会指向 ~/venv/bin/pip,跟系统彻底隔离。这是最干净的方案,新项目无脑用。
方案二:pipx,适合命令行工具。 只装给你敲命令用的东西(black、httpie、pre-commit),每个包自带独立环境,不会污染主环境:
pipx install black
它底层也是 venv,但自动帮你建好、你不用管激活。
方案三:--user,装到用户目录。 不需要 sudo,也不需要 venv,包落在 ~/.local/lib/python3.12/site-packages:
pip3 install --user markdown
这个方案很轻,但有两个前提得注意:一是 pip 自己也可能被包管理管住,如果连 pip3 install --user 都报同样的错,就得叠 --break-system-packages;二是 ~/.local/bin 得在 PATH 里,否则装了命令也敲不出来。
方案四:--break-system-packages,我这次用的。 就是把上面那道锁强行撬开:
pip3 install --user --break-system-packages markdown pyyaml
能装上,但心里得有点数:它只是让 pip 不再检查标记文件,包照样往系统 Python 的搜索路径里塞。如果哪天 apt 更新了 python3 本体,这些第三方包可能跟着一起进黑洞。生产环境我不这么干,这台是纯静态博客构建机,除了跑 build.py 没别的 Python 依赖,炸了也就重来一次——所以认了。
顺带一提,如果依赖在 apt 里有现成的,优先 apt:
apt install python3-yaml
Debian 系打包了一堆 Python 包在 python3-xxx 里,版本偏旧但胜在和系统一致,不会打架。
第二个坑:装成功了,import 不到
解决了安装问题,紧接着又撞一个。跑 python3 build.py 报 ModuleNotFoundError: No module named 'yaml'——包明明装上了。
查了一下路径:
$ which -a python3
/usr/local/bin/python3
/usr/bin/python3
$ which -a pip3
/usr/local/bin/pip3
$ python3 -c "import sys; print(sys.executable); print('\n'.join(sys.path))"
/usr/bin/python3
['', '/usr/lib/python312.zip', '/usr/lib/python3.12', '/usr/lib/python3.12/lib-dynload',
'/usr/local/lib/python3.12/dist-packages', '/usr/lib/python3/dist-packages']
问题清楚了:python3 解析到的是 /usr/bin/python3,但 pip3 是 /usr/local/bin/pip3,它把包装进了 /usr/local/lib/python3.12/site-packages。而系统 python3 的 sys.path 里只有 dist-packages,没有 site-packages。这俩不是一个 Python。
如果你走了 pip install --user,理论上应该装到 ~/.local/lib/python3.12/site-packages,这个目录默认在 sys.path 里,也不在上面的输出中——说明 --user 那次压根没生效,或者 pip 把它降级成了全局安装。
验证办法就一句话:
python3 -c "import sys; print(sys.executable); print(sys.path)"
然后确认 pip 的落点:
pip3 -V # 看清 First/Second/Third 行是哪个 python
python3 -m pip -V # 永远用 -m 形式调用 pip,能避免 90% 的错配
python3 -m pip 这个写法我以前不理解为什么不直接 pip,后来吃了两次亏就懂了:它保证用的是"当前这个 python3",不会因为 PATH 顺序变了就用错解释器。系统重装了、源码编译装了新版 Python、或者 pyenv 切了版本,pip 这个独立入口还指着老版本,-m 就不会。
第五个坑:装是装上了,PATH 里没有
用 --user 装还有个连带问题。包本体进了 ~/.local/lib/python3.12/site-packages,import 是没问题的;但如果装的是一个带命令行入口的包(pipx 那类,或者直接装的 black、httpie),可执行文件落在 ~/.local/bin。
这个目录默认不一定在 PATH 里。装完敲一下:
$ which black
/usr/bin/bash: line 1: black: command not found
别急着说包坏了,先看文件到底在不在:
ls ~/.local/bin/
在的话就是 PATH 问题,两个解法。临时加:
export PATH="$HOME/.local/bin:$PATH"
或者写进 ~/.bashrc 永久生效(记得 source ~/.bashrc)。这台机器上我直接改的是 ~/.profile,因为有些非交互式执行路径不读 .bashrc:
grep -q '.local/bin' ~/.profile || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.profile
我现在的做法
这台机器上:apt 装 python3-yaml(原生),markdown 和 pyyaml 用 pip3 install --user --break-system-packages,装完立刻 python3 -c "import yaml, markdown" 验证一遍。以后在这台机器上写任何 Python 脚本,我都默认先 python3 -m pip install --user,忘了加 -m 就当自己没配好环境。
现在建博客的脚本我已经固定成一个 deploy.sh:
#!/bin/bash
set -e
cd /home/ubuntu/blog-src
python3 build.py
sudo rm -rf /var/www/blog/*
sudo cp -r dist/. /var/www/blog/
sudo chown -R www-data:www-data /var/www/blog
set -e 不能省。以前少了这一行,build.py 抛异常脚本照样往下跑,结果 dist/ 里是上一版残留内容,cp 上去之后线上看着"更新成功了"其实根本没变。静态站点部署最怕这种静默失败——HTTP 200 给你看,内容却是旧的,能排查半小时。
如果你也在 Ubuntu 上被这个报错卡过,先跑 ls /usr/lib/python3.*/EXTERNALLY-MANAGED 确认一下是不是同一个原因,别急着降级 pip——降了也没用。