项目依赖分析:先摸清家底,再谈瘦身

做库瘦身的第一步是什么?不是直接删代码,也不是盲目换轻量替代品。你想想看,连项目里到底引了哪些库、它们之间什么关系都不清楚,怎么敢动手?我在早期一个数据可视化项目上就吃过这个亏——看着 requirements.txt 里只有 20 行,结果生产环境一部署,pip install 跑了三分钟。后来一查,好家伙,背后拖进来 80 多个间接依赖。

说白了,依赖分析就是给项目做个“体检”,看看哪些库是必需的,哪些是冗余的,哪些版本冲突了。这章我会带你熟悉两个趁手的工具:pip freeze 和 pipdeptree,以及几个常用辅助命令。

1. pip freeze:最基础的依赖快照

pip freeze 应该是每个 Python 开发者都用过的命令。它会把当前环境中所有已安装的包(包括间接依赖)以 包名==版本号 的格式列出来。

pip freeze > requirements.txt

嗯,这条命令简单到不能再简单。但我要提醒一句:千万别直接在全局环境里跑,否则你会把系统级包也带进去。个人习惯是在虚拟环境或项目隔离环境里执行。

我曾经帮一个同事排查问题,他拿全局环境的 freeze 清单部署到 CI,结果少了好几个底层依赖。为什么?因为全局环境里有某个库的 C 扩展包,而新环境没有。从那以后,我每次都在项目根目录下先激活虚拟环境,再执行 freeze。

输出格式长这样:

Flask==2.3.2
Jinja2==3.1.2
MarkupSafe==2.1.3
Werkzeug==2.3.3
click==8.1.3
...

你看,只列了包名和版本,没有层级关系。如果项目依赖了 Flask,Flask 依赖了 Werkzeug,这里全都平铺显示。你很难一眼看出谁是直接依赖、谁是间接依赖。所以,纯靠 pip freeze 做瘦身分析,不够用。

小技巧:如果想生成只包含顶层依赖(直接引入的包)的文件,可以用 pip freeze --exclude-editable 过滤可编辑安装的包,但依然无法去重间接依赖。更可靠的做法结合下面的工具。

2. pipdeptree:看清依赖的“族谱”

pipdeptree 才是真正帮你理清依赖关系的利器。它把包之间的父子关系用树状图展示出来,你想跳过哪个间接依赖,一目了然。

先安装它:

pip install pipdeptree

然后运行:

pipdeptree

输出示例:

Flask==2.3.2
├── click==8.1.3 [required: >=8.0, installed: 8.1.3]
├── importlib-metadata==6.7.0 [required: >=3.6, installed: 6.7.0]
│   └── zipp==3.15.0 [required: >=3.6, installed: 3.15.0]
├── itsdangerous==2.1.2 [required: >=2.0, installed: 2.1.2]
├── Jinja2==3.1.2 [required: >=3.0, installed: 3.1.2]
│   └── MarkupSafe==2.1.3 [required: >=2.0, installed: 2.1.3]
└── Werkzeug==2.3.3 [required: >=2.3.0, installed: 2.3.3]
    └── MarkupSafe==2.1.3 [required: >=2.0, installed: 2.1.3]

pipdeptree==2.10.0
setuptools==68.0.0
wheel==0.40.0

看到了吗?Flask 下面挂了 click、Jinja2、Werkzeug,而 Jinja2 又依赖 MarkupSafe。MarkupSafe 被两个父包同时引用,但树里会重复显示(安装一遍即可)。这种结构让我能快速判断:哪些包是“根”节点(直接依赖),哪些是“叶”节点(只被一个上层引用)。

我在一个 API 项目里遇到过这样的坑:requests 和 httpx 都依赖了 certifi,但 pipdeptree 显示它们版本要求冲突。最后我统一了这两个 HTTP 库的版本,才解决。没有树状视图,这种冲突很难定位。

常用参数速查

参数 用途 示例
--packages 只显示指定包及其依赖树 pipdeptree --packages Flask
--reverse 反向查看某个包被哪些上层包依赖 pipdeptree --reverse --packages certifi
--warn 显示冲突警告(默认开启) pipdeptree --warn silence
--json 输出 JSON 格式,方便脚本处理 pipdeptree --json
核心能力:pipdeptree --reverse 可以快速找到“被多个上层依赖的公共包”,这些包往往是瘦身的突破口——如果干掉一个上层依赖,可能同时删掉多个间接依赖。

3. 其他辅助工具

除了上面两个主力选手,我还常搭配以下命令做深度分析:

  • pip check:检查当前环境有无版本冲突或缺失依赖。我每次升级项目之前都会跑一遍,避免“升级一时爽,修复火葬场”。
  • pip-licenses:一个第三方工具,可以列出所有包及其许可证,方便合规审查。瘦身时顺便看看有没有不再维护的包。
  • pip show [包名]:查看单个包的详细信息,包括依赖关系、安装路径。我经常用它来核实某个间接依赖的来源。
避坑指南:我曾经用 pip freeze 生成清单后直接删了 requirements.txt 里不认识的包,结果导致 CI 构建失败。后来才意识到,有些间接依赖虽然没直接写进代码里,但运行时会被底层库加载。所以,一定要先看 pipdeptree 的树状结构,分清主次。

4. 一张图看懂依赖分析流程

下面这张流程图概括了我们做依赖分析时的完整链路:

依赖分析流程与工具图谱 Python 项目 pip freeze 所有包平面列表 pipdeptree 树状依赖结构 pip check 版本冲突检测 --reverse 谁引用了该包 pip-licenses 许可证合规检查 依赖清单 & 瘦身依据 确定可删除/替换的包

5. 实战:分析一个真实项目的依赖

假设你接手了一个 Flask+requests+celery 的项目。我会这样做:

  1. 进入虚拟环境:source venv/bin/activate
  2. 生成完整树状结构:pipdeptree > deptree.txt
  3. 找出所有顶层包:看树的第一层,手动整理或使用 pipdeptree --packages Flask requests celery 只看你关心的。
  4. 检查冲突:pip check,如果报错立刻记录。
  5. 反向查找公共依赖:比如 pipdeptree --reverse --packages six,如果 six 被 10 个包引用,那想干掉它很难;如果只有一个废弃包在引用,就可以考虑替换那个废弃包。
  6. 形成瘦身建议:哪些包只被一个不常用的库引用,且那个库有更轻量的代替品。

我记得有个项目里用了 Pillow 只是做图片格式转换,但全量安装带来了大量编解码模块。通过 pipdeptree 发现它没有被其他包依赖,最后我们用 pillow-simd 替代,体积缩小了 40%。

核心原则:依赖分析不是为了删光所有包,而是为了搞清楚每个包的“存在理由”。如果一个包出现在树里,但你找不到任何 import 语句用到它,那它多半是某些包的间接依赖。这时候需要问:这个间接依赖能否被替换?

总结表格:工具对比

工具 输出形式 主要用途 适合场景
pip freeze 平面列表 快速导出版本锁定文件 CI/CD 部署、环境复刻
pipdeptree 树状结构 分析依赖层级、冲突、冗余 瘦身调研、依赖清理
pip check 冲突报告 验证环境一致性 升级前后检查、代码评审
pip show 包详情 查单个包的依赖和位置 定位具体问题包
工作流建议:每次接入新项目,第一件事就是在项目根目录下跑 pipdeptree 并将结果保存到版本库。这样后续任何依赖变动都有迹可循,瘦身的时候能更快定位“谁拖累了你”。