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-deprecatedlegacy-resolver的反面成了默认它会尝试回溯并找到一组兼容的版本。如果你还在用旧版 pip赶紧升级python-mpipinstall--upgradepip这里有个坑当你同时安装多个包时pip 的解析顺序会影响结果。比如pip install requests2.28.0 urllib31.26.0如果 requests 依赖 urllib31.26.0那没问题但如果 requests 依赖 urllib32.0.0pip 就会报冲突。别想着手动指定版本来绕过这只会让问题更隐蔽。锁定文件别再手写 requirements.txt 了手写 requirements.txt 是新手最容易犯的错误之一。你写flask2.3.0但 Flask 依赖的 Werkzeug 版本呢Jinja2 版本呢这些子依赖的版本没有被锁定下次部署时 pip 可能会装到不同的版本导致行为不一致。正确的做法是用pip freeze生成锁定文件pip freezerequirements.txt但pip freeze有个问题——它会输出当前环境中所有已安装的包包括那些你根本没在项目里用到的。更好的方案是用pip-compile来自 pip-tools 包# 先安装 pip-toolspipinstallpip-tools# 创建一个 requirements.in只写顶层依赖echoflask2.3.0requirements.inechorequests2.31.0requirements.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 click8.1.7 # via flask flask2.3.0 # via -r requirements.in itsdangerous2.1.2 # via flask jinja23.1.2 # via flask markupsafe2.1.3 # via jinja2 requests2.31.0 # via -r requirements.in werkzeug2.3.7 # via flask部署时直接用这个文件安装保证环境一致。别这样写pip install -r requirements.txt然后手动改版本号——你改了一个可能就破坏了依赖树的平衡。私有源配置公司内部的 PyPI很多公司有自己内部的包仓库用来存放私有包或者镜像 PyPI。配置私有源有两种方式。第一种在pip install时指定--index-urlpipinstallmy-private-package --index-url https://pypi.company.com/simple/但这样会覆盖默认的 PyPI 源导致你无法安装公开包。更常见的做法是用--extra-index-urlpipinstallmy-private-package --extra-index-url https://pypi.company.com/simple/这样 pip 会先查私有源找不到再去 PyPI。第二种配置pip.confLinux/Mac 在~/.config/pip/pip.confWindows 在%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是必须的——如果私有源用的是自签名证书或者 HTTPpip 会拒绝连接。别为了省事直接设--trusted-host为*这等于关掉了安全验证。有个更优雅的方式用--index-url指向私有源然后通过--find-links指定 PyPI 的镜像。这样私有包走私有源公开包走镜像互不干扰。安全审计别装到恶意包2023 年 PyPI 上被下架的恶意包超过 5000 个。最常见的攻击手法是 typosquatting——把包名起得和知名包很像比如requsts少了个 e冒充requestsurllib3冒充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里每个包后面会跟一串--hashsha256:...。部署时用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 等去做。
006、pip 包管理进阶:依赖解析、锁定文件、私有源配置与安全审计
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-deprecatedlegacy-resolver的反面成了默认它会尝试回溯并找到一组兼容的版本。如果你还在用旧版 pip赶紧升级python-mpipinstall--upgradepip这里有个坑当你同时安装多个包时pip 的解析顺序会影响结果。比如pip install requests2.28.0 urllib31.26.0如果 requests 依赖 urllib31.26.0那没问题但如果 requests 依赖 urllib32.0.0pip 就会报冲突。别想着手动指定版本来绕过这只会让问题更隐蔽。锁定文件别再手写 requirements.txt 了手写 requirements.txt 是新手最容易犯的错误之一。你写flask2.3.0但 Flask 依赖的 Werkzeug 版本呢Jinja2 版本呢这些子依赖的版本没有被锁定下次部署时 pip 可能会装到不同的版本导致行为不一致。正确的做法是用pip freeze生成锁定文件pip freezerequirements.txt但pip freeze有个问题——它会输出当前环境中所有已安装的包包括那些你根本没在项目里用到的。更好的方案是用pip-compile来自 pip-tools 包# 先安装 pip-toolspipinstallpip-tools# 创建一个 requirements.in只写顶层依赖echoflask2.3.0requirements.inechorequests2.31.0requirements.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 click8.1.7 # via flask flask2.3.0 # via -r requirements.in itsdangerous2.1.2 # via flask jinja23.1.2 # via flask markupsafe2.1.3 # via jinja2 requests2.31.0 # via -r requirements.in werkzeug2.3.7 # via flask部署时直接用这个文件安装保证环境一致。别这样写pip install -r requirements.txt然后手动改版本号——你改了一个可能就破坏了依赖树的平衡。私有源配置公司内部的 PyPI很多公司有自己内部的包仓库用来存放私有包或者镜像 PyPI。配置私有源有两种方式。第一种在pip install时指定--index-urlpipinstallmy-private-package --index-url https://pypi.company.com/simple/但这样会覆盖默认的 PyPI 源导致你无法安装公开包。更常见的做法是用--extra-index-urlpipinstallmy-private-package --extra-index-url https://pypi.company.com/simple/这样 pip 会先查私有源找不到再去 PyPI。第二种配置pip.confLinux/Mac 在~/.config/pip/pip.confWindows 在%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是必须的——如果私有源用的是自签名证书或者 HTTPpip 会拒绝连接。别为了省事直接设--trusted-host为*这等于关掉了安全验证。有个更优雅的方式用--index-url指向私有源然后通过--find-links指定 PyPI 的镜像。这样私有包走私有源公开包走镜像互不干扰。安全审计别装到恶意包2023 年 PyPI 上被下架的恶意包超过 5000 个。最常见的攻击手法是 typosquatting——把包名起得和知名包很像比如requsts少了个 e冒充requestsurllib3冒充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里每个包后面会跟一串--hashsha256:...。部署时用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 等去做。