006、pip 包管理进阶:依赖解析、锁定文件、私有源配置与安全审计
上周五晚上十一点,生产环境突然炸了。一个同事在本地跑通了新功能,pip install 一切正常,结果部署到服务器上,Python 解释器直接报ModuleNotFoundError: No module named 'cryptography'。我盯着 requirements.txt 看了三分钟——里面确实没写 cryptography,但本地环境里它作为某个包的依赖被自动装上了。服务器上那个依赖包的版本不同,恰好没带 cryptography。这种坑,踩过一次就再也不敢只写个pip install -r requirements.txt就跑路了。
依赖解析:pip 到底在干什么
很多人以为pip install flask就是装一个 Flask,实际上 pip 要干的事远比你想象的复杂。它会去 PyPI 上拉取 Flask 的元数据,看看它依赖什么(比如 Jinja2、Werkzeug、click),然后递归地解析这些依赖的依赖,最后形成一个完整的依赖树。这个过程叫依赖解析。
但 pip 的解析器有个历史遗留问题——它默认用的是旧版解析器,遇到冲突时不会主动回溯,而是直接报错或者装一个不兼容的版本。从 pip 21.3 开始,新版解析器(--use-deprecated=legacy-resolver的反面)成了默认,它会尝试回溯并找到一组兼容的版本。如果你还在用旧版 pip,赶紧升级:
python-mpipinstall--upgradepip这里有个坑:当你同时安装多个包时,pip 的解析顺序会影响结果。比如pip install requests==2.28.0 urllib3==1.26.0,如果 requests 依赖 urllib3>=1.26.0,那没问题;但如果 requests 依赖 urllib3>=2.0.0,pip 就会报冲突。别想着手动指定版本来绕过,这只会让问题更隐蔽。
锁定文件:别再手写 requirements.txt 了
手写 requirements.txt 是新手最容易犯的错误之一。你写flask==2.3.0,但 Flask 依赖的 Werkzeug 版本呢?Jinja2 版本呢?这些子依赖的版本没有被锁定,下次部署时 pip 可能会装到不同的版本,导致行为不一致。
正确的做法是用pip freeze生成锁定文件:
pip freeze>requirements.txt但pip freeze有个问题——它会输出当前环境中所有已安装的包,包括那些你根本没在项目里用到的。更好的方案是用pip-compile(来自 pip-tools 包):
# 先安装 pip-toolspipinstallpip-tools# 创建一个 requirements.in,只写顶层依赖echo"flask==2.3.0">requirements.inecho"requests==2.31.0">>requirements.in# 编译生成锁定文件pip-compile requirements.in这会生成一个requirements.txt,里面包含了所有子依赖的精确版本号,并且会标注每个包是由哪个顶层依赖引入的。比如:
# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # pip-compile requirements.in click==8.1.7 # via flask flask==2.3.0 # via -r requirements.in itsdangerous==2.1.2 # via flask jinja2==3.1.2 # via flask markupsafe==2.1.3 # via jinja2 requests==2.31.0 # via -r requirements.in werkzeug==2.3.7 # via flask部署时直接用这个文件安装,保证环境一致。别这样写:pip install -r requirements.txt然后手动改版本号——你改了一个,可能就破坏了依赖树的平衡。
私有源配置:公司内部的 PyPI
很多公司有自己内部的包仓库,用来存放私有包或者镜像 PyPI。配置私有源有两种方式。
第一种,在pip install时指定--index-url:
pipinstallmy-private-package --index-url https://pypi.company.com/simple/但这样会覆盖默认的 PyPI 源,导致你无法安装公开包。更常见的做法是用--extra-index-url:
pipinstallmy-private-package --extra-index-url https://pypi.company.com/simple/这样 pip 会先查私有源,找不到再去 PyPI。
第二种,配置pip.conf(Linux/Mac 在~/.config/pip/pip.conf,Windows 在%APPDATA%\pip\pip.ini):
[global] index-url = https://pypi.org/simple/ extra-index-url = https://pypi.company.com/simple/ [install] trusted-host = pypi.company.com这里trusted-host是必须的——如果私有源用的是自签名证书或者 HTTP,pip 会拒绝连接。别为了省事直接设--trusted-host为*,这等于关掉了安全验证。
有个更优雅的方式:用--index-url指向私有源,然后通过--find-links指定 PyPI 的镜像。这样私有包走私有源,公开包走镜像,互不干扰。
安全审计:别装到恶意包
2023 年 PyPI 上被下架的恶意包超过 5000 个。最常见的攻击手法是 typosquatting——把包名起得和知名包很像,比如requsts(少了个 e)冒充requests,urllib3冒充urllib3(注意是数字 1 而不是字母 l)。
用pip install之前,先检查一下包名。我习惯用pip search或者直接去 PyPI 官网看。但更靠谱的是用pip-audit工具:
pipinstallpip-audit pip-audit-rrequirements.txt它会扫描你的依赖,和已知漏洞数据库(比如 GitHub Advisory Database)比对,告诉你哪些包有已知漏洞。比如:
No known vulnerabilities found或者:
Found 2 known vulnerabilities in 1 package Name Version ID Fix Versions -------- --------- ------------------- ------------- flask 2.2.0 GHSA-xxxx-xxxx-xxxx 2.2.1, 2.3.0别等到出事了才跑审计。我每次合并代码前都会在 CI 里加一步pip-audit,如果发现高危漏洞直接阻断合并。
还有一个容易被忽略的点:requirements.txt里的哈希校验。pip 支持--require-hashes参数,安装时会校验每个包的哈希值,防止中间人攻击。生成带哈希的锁定文件:
pip-compile --generate-hashes requirements.in生成的requirements.txt里每个包后面会跟一串--hash=sha256:...。部署时用pip install --require-hashes -r requirements.txt,任何哈希不匹配都会报错。这招对生产环境尤其重要——你永远不知道 PyPI 的 CDN 会不会被劫持。
个人经验
依赖管理这件事,越早自动化越好。别等到生产环境炸了才想起用 pip-compile。我现在的标准流程是:项目初始化时用pip-compile生成锁定文件,每次加新依赖都编辑requirements.in然后重新编译,CI 里跑pip-audit和pip install --require-hashes。看起来多花了几分钟,但省掉的是半夜爬起来修 bug 的时间。
另外,别迷信pip freeze。它输出的东西太杂,而且会把一些系统级的包也列进去。用 pip-tools 或者 Poetry 这种工具,它们的设计思路就是让你只管理顶层依赖,子依赖交给工具去锁。
最后一条:永远不要在pip install后面加sudo。如果你需要全局安装,用pip install --user或者直接用虚拟环境。系统级的 Python 包管理,交给系统包管理器(apt、brew 等)去做。