pipreqs 实战指南:基于项目 import 自动生成 requirements.txt 的完整方案

原创2026-09-27 23:59:011,260 阅读
文章标签:开发工具CLI

pipreqs 实战指南:基于项目 import 自动生成 requirements.txt 的完整方案

本文围绕开源项目 pipreqs 的官方文档(见 README.rst 与 docs/readme.rst)展开,结合仓库源码 pipreqs/pipreqs.py 与测试用例 tests/test_pipreqs.py,系统讲解如何不依赖 pip freeze、只根据项目代码中真实出现的 import 语句来生成 requirements.txt。读完本文,你将掌握 pipreqs 的全部命令行参数、底层实现原理,以及 --mode、--diff、--clean、Jupyter Notebook 扫描等高级用法,能够快速为任意 Python 项目产出干净、可复现的依赖清单。

一、为什么不用 pip freeze,而选 pipreqs

官方 README 用三条理由直接点明了 pipreqs 与传统 pip freeze 路线的本质区别:

  • pip freeze 只会保存当前环境中通过 pip install 安装的包,与项目代码实际用到的模块无关;
  • pip freeze 会把环境里所有包全部写入(如果没有 virtualenv 隔离),其中包含大量你当前项目根本用不到的包,导致 requirements.txt 臃肿且难以迁移;
  • 某些场景你只是想为新项目快速生成 requirements.txt,根本不需要先安装这些模块。

pipreqs 的思路恰好相反:它直接扫描项目源码中的 import / from ... import ... 语句,按“代码真正引用了什么”来生成依赖清单。项目主页描述也印证了这一设计初衷——“Generate pip requirements.txt file based on imports of any project”(见 pyproject.toml)。

二、安装

1. 标准安装

pip install pipreqs

该方式会一并安装支持 Jupyter Notebook 扫描所需的依赖(nbconvert、ipython),对应 pyproject.toml 中声明的依赖项:yarg>=0.1.9、docopt>=0.6.2、nbconvert>=7.11.0、ipython>=8.12.3。安装完成后会注册 pipreqs 命令行入口(pipreqs = "pipreqs.pipreqs:main")。

2. 精简安装(不需要 Notebook 支持时)

如果你不需要扫描 Jupyter Notebook,可以跳过相关依赖,只装核心组件:

pip install --no-deps pipreqs
pip install yarg==0.1.9 docopt==0.6.2

注意:这样安装后如果仍使用 --scan-notebooks,程序会抛出 NbconvertNotInstalled 异常,提示需要安装 nbconvert 与 ipython(见 pipreqs/pipreqs.py 中 handle_scan_noteboooks 的实现)。

3. Python 版本要求

根据 pyproject.toml,pipreqs 要求 Python >=3.9, <3.14,支持 3.9~3.13。

三、快速上手:一条命令生成 requirements.txt

最简单的用法是传入项目目录路径:

$ pipreqs /home/project/location
Successfully saved requirements file in /home/project/location/requirements.txt

不传路径时默认扫描当前工作目录(源码中 input_path is None 时回退为 os.path.abspath(os.curdir),见 pipreqs/pipreqs.py)。默认输出文件即为项目目录下的 requirements.txt,内容形如:

wheel==0.23.0
Yarg==0.1.9
docopt==0.6.2

生成的文件按包名(忽略大小写)排序,与 pip freeze 的排序风格一致(排序逻辑见 init 函数中的 sorted(imports, key=lambda x: x["name"].lower()))。

四、命令行参数全解析

官方文档给出的完整用法如下:

Usage:
    pipreqs [options] [<path>]

Arguments:
    <path>                The path to the directory containing the application files for which a requirements file
                          should be generated (defaults to the current working directory)

各选项的作用与实战说明:

选项 说明 实战要点
--use-local 仅使用本地已安装包的信息,不查询 PyPI 离线环境首选;只列出本机 site-packages 中能找到的包
--pypi-server <url> 使用自定义 PyPI 服务器 默认服务器为 https://pypi.python.org/pypi/,可用于私有镜像
--proxy <url> 使用代理,参数会传给 requests 库 也可通过环境变量设置 HTTP_PROXY / HTTPS_PROXY
--debug 打印调试信息 等价于把日志级别调为 DEBUG
--ignore <dirs>... 忽略额外目录,多个目录用逗号分隔 例如 --ignore .ignored_dir,.ignore_second
--no-follow-links 不跟随项目中的符号链接 防止扫描误入链接指向的外部目录
--ignore-errors 扫描文件时忽略错误 遇到语法错误文件时打印堆栈并继续,而不是直接抛异常退出
--encoding <charset> 指定文件打开编码 默认 utf-8
--savepath <file> 将依赖清单保存到指定文件 与默认的 项目目录/requirements.txt 不同
--print 将依赖清单输出到标准输出 不写文件,方便管道操作
--force 覆盖已存在的 requirements.txt 默认情况下文件已存在会拒绝写入
--diff <file> 对比指定文件中的模块与项目实际 import 只报告差异,不修改文件
--clean <file> 清理指定文件,删除项目未 import 的模块 直接改写该文件
--mode <scheme> 动态版本方案:compat、gt 或 no-pin 见下文“动态版本控制”小节
--scan-notebooks 同时扫描 Jupyter Notebook(.ipynb)文件中的 import 需 nbconvert / ipython 支持

代理的两种配置方式

pipreqs /path/to/project --proxy http://10.10.1.10:3128

或者在终端设置环境变量:

$ export HTTP_PROXY="http://10.10.1.10:3128"
$ export HTTPS_PROXY="https://10.10.1.10:1080"

源码中 --proxy 会构造成 {"http": url, "https": url} 字典直接传给 requests,而环境变量方案则由 requests 库自身识别。

五、深入源码:import 是如何变成 requirements.txt 的

要真正用好 pipreqs,理解它的四步流水线会很有帮助。整体流程都在 pipreqs/pipreqs.py 中,入口为 init()。

1. 扫描目录并做 AST 解析(get_all_imports)

get_all_imports() 使用 os.walk 递归遍历目标目录,并做三件关键事:

  • 默认忽略目录:.hg、.svn、.git、.tox、__pycache__、env、venv、.venv、.ipynb_checkpoints,这些目录不会被扫描;
  • 支持 --ignore 追加忽略目录:额外目录会先取 os.path.basename(os.path.realpath(e)) 归一化后再加入忽略列表;
  • 只处理指定扩展名:默认 [".py", ".pyw"],开启 --scan-notebooks 后追加 .ipynb。

对每个文件,pipreqs 用 Python 标准库 ast.parse 把源码解析成抽象语法树,再遍历所有 ast.Import 和 ast.ImportFrom 节点收集模块名,而不是用正则匹配文本,因此能准确处理 import os.path as test、from sys import argv 这类写法。测试用例 tests/_data/test.py 正是用大量“花式 import”验证了这一能力。

收集到的原始模块名会做两步清洗:

  1. 只保留 . 之前的第一段(from django.conf import ... 只记 django);
  2. 与“项目自身候选模块”求差集(目录名、.py 文件名本身不算外部依赖)。

2. 剔除标准库模块

清洗后的模块集合与 pipreqs/stdlib(1785 行的标准库模块清单)做差集,time、logging、os、sys 等标准库模块会被自动排除。测试 test_get_all_imports 明确断言了 time、logging、curses、__future__、django、models 不会出现在结果中(见 tests/test_pipreqs.py)。

3. 导入名到 PyPI 包名的映射(get_pkg_names)

import 名并不总等于 pip 包名。pipreqs 内置了一张映射表 pipreqs/mapping,例如:

  • bs4:beautifulsoup4
  • cv2:opencv-python
  • django:Django
  • PIL:Pillow
  • flask:Flask
  • MySQLdb:MySQL-python
  • bson:pymongo

映射不存在时才直接使用 import 名本身。这张表还天然完成了依赖去重:比如 tests/_data_duplicated_deps/db.py 同时 import pymongo 和 from bson.objectid import ObjectId,由于 bson 被映射到 pymongo,最终只生成一行 pymongo==x.x.x(测试 test_deduplicate_dependencies 验证了这一点)。

4. 解析版本号(本地包 + PyPI 查询)

版本号解析分两种模式:

  • --use-local(仅本地):get_locally_installed_packages() 遍历 sys.path,读取各包目录下的 top_level.txt,把“包名 + 版本 + 导出的顶层模块”配对;get_import_local() 再按 import 名匹配包名或导出模块,返回本地包信息。离线、或不想访问公网时使用。
  • 默认模式(本地 + PyPI):先在本地查找,查不到的 import 名进入 get_imports_info(),向 {pypi_server}{包名}/json 发起请求,用 yarg 库解析 JSON 拿到 latest_release_id 作为版本号。包不存在或网络异常时只打警告并跳过该包。

源码在解析完成后会输出提示,要求人工核对最终清单,避免 import 名与包名映射带来的依赖混淆。此外,结果列表统一按包名(小写)排序后再写入文件。

六、高级用法详解

1. 动态版本控制:--mode

默认生成固定版本(==)。需要更灵活的版本约束时使用 --mode,支持三种方案(见 pipreqs/pipreqs.py 的 dynamic_versioning()):

scheme 生成格式 示例
默认(不传 --mode) == 精确锁定 Flask==1.1.2
compat ~= 兼容版本 Flask~=1.1.2
gt >= 下限版本 Flask>=1.1.2
no-pin 不写版本号 Flask

传入非法 scheme 会抛出 ValueError,提示只能用 compat、gt 或 no-pin。测试 test_dynamic_version_no_pin_scheme、test_dynamic_version_gt_scheme、test_dynamic_version_compat_scheme 分别验证了三种输出格式(见 tests/test_pipreqs.py)。

2. 依赖体检:--diff 与 --clean

这两个选项都基于 parse_requirements()(解析已有 requirements 文件,支持 <、>、=、!、~ 等分隔符,自动跳过注释与空行)和 compare_modules()(求“文件中模块 − 项目 import 模块”的差集):

  • --diff <file>:只打印“在文件中但项目里没用到”的模块清单,不改动文件;
  • --clean <file>:直接把这类多余模块从文件中删除。若没有多余模块,则输出 Nothing to clean in <file>。

测试 test_clean_with_imports_to_clean 演示了完整流程:先用 --force 重新生成 requirements.txt,再在另一个不含 sqlalchemy 的项目上执行 --clean,最终文件中不再出现 sqlalchemy。

3. 离线场景:--use-local

pipreqs /path/to/project --use-local

只输出本机 sys.path 中能解析到的包,适合无外网环境或需要快速出结果的场景。测试 test_init_local_only 验证了输出项全部落在本地已安装包集合内。

4. 自定义 PyPI 与代理

pipreqs /path/to/project --pypi-server https://mirror.example.com/pypi/
pipreqs /path/to/project --proxy http://proxy.example.com:8080

--pypi-server 会替换默认的 https://pypi.python.org/pypi/;测试 test_custom_pypi_server 也验证了传入非法 server(如 nonexistent)时会按 requests 的规则抛错。企业内网或镜像站场景非常实用。

5. 忽略目录、符号链接与容错

pipreqs /path/to/project --ignore tests,docs,migrations
pipreqs /path/to/project --no-follow-links
pipreqs /path/to/project --ignore-errors
  • --ignore 用逗号分隔多个目录名,测试 test_ignored_directory 验证了 .ignored_dir,.ignore_second 中 import 的 click、getpass 不会进入结果;
  • --no-follow-links 对应 os.walk(..., followlinks=False),避免跟随符号链接导致扫描范围失控;
  • --ignore-errors 在文件解析失败时打印堆栈并继续(测试 test_ignore_errors:对含语法错误的项目也能返回空结果),而不带该选项时遇到非法文件会直接抛异常(测试 test_invalid_python 断言抛出 SyntaxError)。

6. 扫描 Jupyter Notebook:--scan-notebooks

pipreqs /path/to/project --scan-notebooks

开启后,.ipynb 文件会先通过 nbconvert 的 PythonExporter 转成 Python 源码再走 AST 解析(见 ipynb_2_py())。仓库测试用 tests/_data_notebook 目录验证了三点:

  • test_import_notebooks:能正确提取 notebook 代码单元格中的 import,同时排除 magic 命令(如 %matplotlib inline)等非代码内容;
  • test_ipynb_2_py:同一批 import 在 .py 与 .ipynb 中扫描结果完全一致(对比 tests/_data/test.py 与 tests/_data_notebook/test.ipynb);
  • test_ignore_notebooks:未开启该选项时 notebook 不被扫描,生成的 requirements 为空。

注意:按 pyproject.toml,nbconvert 与 ipython 已是标准依赖,若用 --no-deps 精简安装则需手动补装,否则会报 NbconvertNotInstalled。

7. 输出控制:--savepath、--print、--force

  • --savepath <file>:把结果写到指定文件(测试 test_init_savepath 使用 requirements2.txt 验证);
  • --print:把结果打印到标准输出而不落盘(测试 test_output_requirements 断言 stdout 内容与写入文件的内容完全一致),适合配合 > 重定向到任意位置;
  • --force:覆盖已存在的 requirements 文件。默认行为是拒绝覆盖——当目标文件已存在且未传 --print/--savepath/--force 时,会输出 requirements.txt already exists, use --force to overwrite it 并直接返回(测试 test_init_overwrite 验证文件内容保持不变)。

七、工作原理小结:一次完整的调用链

以 pipreqs /home/project/location 为例,完整链路为:

  1. main() 用 docopt 解析命令行参数,设置日志级别;
  2. init() 确定输入路径、编码、忽略目录、保存路径,并检查文件是否已存在;
  3. get_all_imports() 遍历目录、AST 解析、剔除标准库与项目自身模块;
  4. get_pkg_names() 通过 pipreqs/mapping 完成 import 名 → PyPI 包名映射与去重;
  5. 本地安装包匹配 + PyPI JSON 查询获取版本号,合并后按小写包名排序;
  6. generate_requirements_file() 按所选 symbol(== / ~= / >= / 空)写出文件(无版本信息的包只写包名)。

测试套件 tests/test_pipreqs.py 覆盖了上述全部分支,配合 tests 目录下的各种数据样本(正常、含重复依赖、含非法语法、含 notebook、含 pyw 文件等),可作为你上手验证和二次开发时的参考。

八、使用建议

  • 生成后务必人工核对最终清单:README 和源码都提示,import 名与 PyPI 包名并非一一对应,可能产生“依赖混淆”,尤其是通过 --mode no-pin 或本地查询生成的版本号;
  • 团队协作或 CI 场景,建议结合 --mode gt 或 compat 保留版本弹性,避免 == 锁定过死导致升级困难;
  • 迁移老项目时,先用 --diff 体检、再用 --clean 清理,可以无痛精简历史遗留的 requirements 文件;
  • 在无外网环境,优先 --use-local;在内网有镜像站时,用 --pypi-server 指向镜像,配合 --proxy 解决网络代理问题。

以上能力全部基于 pipreqs 0.5.0 的官方文档(README.rst)、核心实现(pipreqs/pipreqs.py)与测试用例(tests/test_pipreqs.py),可放心按本文步骤在本地复现。

登录后查看全文
pipreqs