Python依赖导出与离线部署:requirements.txt与whl文件全解析
2026/9/10 18:53:51 网站建设 项目流程

先说说最实际的场景:本地开发环境调好的项目,换台电脑、交给同事、部署到服务器,结果一跑就报 ModuleNotFoundError。这时候你才意识到,当初图省事没管依赖,现在要一个一个去 pip list 里对版本,那感觉真是一言难尽。所以,“Python导出requirements.txt以及第三方库whl文件”这件事,说白了就是给项目做一次“环境快照”,把用到的第三方库清单和安装包一次性备份下来,以后不管到哪台机器、什么网络环境,都能照着原样还原。这篇内容适合所有写 Python 的人,不管是刚入门的小白还是经常要交付项目的资深开发,都值得花十分钟把这套流程捋清楚。

我最早接触这需求是因为一个挺尴尬的事:甲方给了台不能上外网的服务器,让我把之前写的爬虫项目部署上去,还说“你那个环境自己搞一下”。我人直接傻了,项目里 import 了 requests、lxml、openpyxl,还用了 scrapy,这台机器啥都没有,连 pip 装包都装不了。后来被逼着研究了一圈,才发现官方早就有解决方案,只是平时大家用不到,就没在意。这篇文章我把整个流程拆开讲,包括导出依赖列表、导出 whl 安装包、离线安装、跨平台部署,顺便把容易踩的坑也列出来,希望能帮大家少走弯路。

1. 内容整体设计与思路拆解

1.1 先分清两个概念:依赖清单和安装包文件

我们平时说的“导出依赖”,其实包含两件不太一样的事。第一件是导出一个文本文件,里面记录项目依赖了哪些第三方库以及对应版本号,这就是 requirements.txt。它就像超市购物清单,只告诉你“要买什么”,但东西不在手边。第二件是把第三方库的实际安装包下载成 .whl 文件存到本地,这才是真正的“货”,拿到任何一台机器上都能直接装。

很多教程会把这两件事混在一起讲,但实际项目里它们是两种不同场景。requirements.txt 适合联网环境下的快速复现,比如团队协作、CI/CD 流水线,拿到清单后 pip install -r requirements.txt 就能装。而 whl 文件适合离线环境、内网部署、或者是那种网络特别不稳定的情况,你把包提前下载好带走,目标机器上完全不需要访问 PyPI。

我在最初的方案设计时,是把这两步拆开的:先确定依赖范围,再决定要不要下载离线包。如果目标环境能联网,就只导出 requirements.txt 就完事;如果目标环境是离线机,那就必须把 requirements.txt 和 whl 文件一起准备好。这个判断决定了整个操作的复杂程度,也是很多人一开始没想清楚导致后面折腾半天的地方。

1.2 为什么不让同事/服务器直接 pip install 全部包

有人可能会说,直接让目标机器装项目根目录里所有 import 过的库不就行了?问题在于,你“项目里用到”的库和“这台机器上已安装”的库往往不是一回事。本地环境里可能装了 pandas、numpy 但项目根本没用,也可能会因为早期实验装了一大堆奇怪版本的包,这些都会被带过去,轻则浪费时间,重则版本冲突把环境搞坏。

所以标准的做法是锁定一个“依赖边界”:要么从当前环境快照(pip freeze),要么从项目代码里扫描 import(pipreqs),把边界内的依赖导出。然后,如果目标是离线部署,就把这些依赖对应的 whl 文件也一起拉下来。这套流程的核心思路就是“按需锁定、离线携带、还原验证”,三个环节缺一个都会出问题。

2. 核心细节解析与实操要点

2.1 pip freeze 导出全量依赖:最快但最“脏”

pip freeze 是最常用的命令,一条指令就能把当前 Python 环境里所有第三方包和版本号列出来。用法也简单:

pip freeze > requirements.txt

生成的文件长这样:

certifi==2023.7.22 charset-normalizer==3.2.0 idna==3.4 requests==2.31.0 urllib3==2.0.4

适合的场景是:这台机器的环境本身就很“干净”,或者你是在虚拟环境里操作的,环境里装的包就是项目需要的全部。我强烈建议在虚拟环境里做这件事,不然很容易把系统级的一堆包全部冻结进去。

它的缺点也明显:不会区分“项目真正用到的”和“环境里多余的”。如果你是在全局环境里直接 freeze,出来的清单可能洋洋洒洒几十上百行,里面一半都没用。如果拿着这份清单到另一台机器上整,轻则多装一堆无用包,重则因为某几个包的版本与新系统不兼容直接安装失败。

2.2 pipreqs 按项目扫描生成:只留真正用到的

如果项目代码结构相对规范,我通常推荐用 pipreqs,它自动扫描项目目录下所有 Python 文件的 import 语句,再结合当前环境查出对应版本号,生成一份只包含“项目实际需要”的清单。用法如下:

pip install pipreqs pipreqs ./ --encoding=utf8 --force

生成的文件也叫 requirements.txt,但内容比 pip freeze 干净得多,只保留代码里 import 过的库。最明显的感受是:项目里可能用了 scrapy 这种重型框架,但本地装了 numpy 只是顺手 pip install 的,pip freeze 会把 numpy 也带进去,pipreqs 就不会。

需要注意几个细节。第一,--encoding=utf8 必须有,尤其 Windows 下默认编码容易报 UnicodeDecodeError。第二,--force 是覆盖已有 requirements.txt 的,不写这个会提示文件已存在导致不生成。第三,pipreqs 是纯静态扫描,import 写在函数内部或者动态拼接模块名时它扫不到,这种情况需要手动在生成的清单里补上。

2.3 pip-tools 锁定间接依赖:进阶方案的“稳”

如果你遇到的情况更复杂,比如项目可能用了某些只声明顶层依赖的库,但它的底层依赖需要精确锁定版本,那 pip freeze 可能过于冗长(把所有间接依赖都带上了),而 pipreqs 又可能缺失间接依赖。这个时候可以用 pip-tools 的 pip-compile 来做。

pip install pip-tools # 先手写一个 requirements.in,里面放项目直接依赖 # 然后编译生成锁定版本的 requirements.txt pip-compile requirements.in

pip-compile 会根据你声明的顶层依赖,去 PyPI 上拉取完整的依赖树并固定版本号,生成一份既“干净”又“完整”的清单。这样做的好处是:生成的 requirements.txt 不依赖当前环境装了什么包,而是根据解析规则计算出来的,理论上在任何机器上都能还原出一致的依赖环境。缺点是步骤相对繁琐,日常小项目你用不上。

2.4 三种方式怎么选:一张表格给你说清楚

方式命令包含范围适用场景缺点
pip freezepip freeze > requirements.txt当前环境所有第三方包虚拟环境整体迁移、快速快照会混入无关包,依赖边界不清晰
pipreqspipreqs ./ --encoding=utf8 --force项目代码 import 过的包交付项目、代码结构清晰静态扫描可能漏掉动态导入
pip-toolspip-compile requirements.in顶层依赖+解析出的依赖树复杂项目、需要精准锁版本步骤较多,需要额外安装工具

我给个经验判断:一两个文件的小脚本,用 pip freeze 然后手动删掉明显无关的包就行;正经项目交付,用 pipreqs;团队协作频繁换环境、或者要发到 PyPI 上的库,用 pip-tools。

3. 实操过程与核心环节实现

3.1 单包导出 whl 文件:pip download 的最基础用法

假设我现在要把 requests 库以及它的所有依赖包下载成 whl 文件到本地,命令如下:

pip download requests -d ./whls

这里 -d 指定输出目录,如果你不指定,默认会下载到当前目录。执行完以后,whls 文件夹里会出现 requests 以及它依赖的 urllib3、certifi、idna、charset-normalizer 等一堆 whl 文件。

为什么要用 pip download 而不是去 PyPI 网站手工下载?因为 pip download 会自动解析依赖关系,把顶层库和所有间接依赖全部一起下载到本地,手工下载你根本分不清哪些包是必须的。而且,pip download 是 pip 自带的功能,不需要额外安装什么工具,Python 3.x 只要带了 pip 就能用。

3.2 批量导出:按 requirements.txt 把环境整体打包

如果已经提前准备好了 requirements.txt,那导出整个环境的 whl 包就变成一条命令的事:

pip download -r requirements.txt -d ./whls

这条命令会逐行读取 requirements.txt,把每一行指定的包和版本都下载到目录里。实际执行时你会看到 pip 在逐个下载并打印进度,最后提示”Successfully downloaded requests-2.31.0 ...“之类的信息。

我在实际项目里通常会先建一个单独目录,比如 project_offline,里面再分两个子目录:requirements 放清单文件,whls 放安装包。这样整个离线交付包的结构很清晰,目标机器拿到手也不会一脸懵。

3.3 跨平台导出:在 Windows 上给 Linux 服务器准备离线包

这一步是最容易出问题的,但也是最有价值的部分。很多人第一次操作时会发现,自己在 Windows 上导出的 whl 文件拿到 Linux 服务器上装不了,报错提示“xxx.whl is not a supported wheel on this platform”。原因很简单:每个 whl 文件名里其实都标注了适用的操作系统和 Python 版本。

如果你想在一台机器上,为目标平台准备另一套平台的 whl 文件,就得给 pip download 指定平台参数。比如我在 Windows 电脑上想给 Linux x86_64 的 Python 3.10 环境准备离线包,命令是:

pip download \ -r requirements.txt \ -d ./whls_linux \ --platform manylinux2014_x86_64 \ --python-version 310 \ --only-binary=:all:

几个参数逐个说。--platform 是指定目标机器平台,manylinux2014_x86_64 覆盖大多数现代 Linux x86_64 服务器,这是目前兼容面最广的标注。--python-version 指目标环境的大版本,写 310 就是 Python 3.10,没必要写完整小版本号。--only-binary=:all: 意思是只下载 whl 格式的预编译包,不下载 tar.gz 源码包,因为源码包在目标机器上还需要编译工具链,我们既然做离线包,就要尽量避免这种不确定性。

但这个方案有个前提:你项目依赖的所有包都必须提供了对应平台的 whl 预编译版本。如果一个包只有源码包,没有 wheel 包,那加上 --only-binary=:all: 之后会直接报错。我实际遇到过一个典型:某个内部小工具库只在 PyPI 上发布了 sdist 源码包,没有发 wheel,用这个命令就会失败。这种情况的处理方式是去掉 --only-binary=:all: 参数,把源码包也下载下来,让目标机器现场编译,前提是目标机器得有编译环境,这点在离线部署时往往很麻烦。

3.4 目标机器离线安装:pip install 的离线模式

离线包准备好以后,目标机器上的安装命令只有一条:

pip install --no-index --find-links=./whls -r requirements.txt

--no-index 的意思是不要访问 PyPI,--find-links=./whls 是指定从本地目录找安装包。这两条必须一起用,否则 pip 可能会去联网检查最新版本,又慢又不可控。

我在服务器上实际执行时,输出里会出现 “Looking in links: ./whls” 表示正在从本地目录检查,然后逐个安装。如果缺了某个包,会明确告诉你 “Could not find a version that satisfies the requirement xxx”,这时候回本地电脑把缺的 whl 补下载一份,拿到目标机器再装就行。

3.5 一个完整迁移案例:从开发机到离线服务器

我把自己踩过坑之后总结的标准流程放这里,照着做基本不会出问题。第一次,先在自己开发机上确认项目能正常跑,用虚拟环境激活命令进入环境。第二步,用 pipreqs 扫描生成 requirements.txt。第三步,检查清单,把 pipreqs 漏掉的人工补进去。第四步,执行 pip download -r requirements.txt -d ./whls 下载离线包。第五步,把 requirements.txt 和 whls 文件夹打包发给目标机器。第六步,在目标机器上建虚拟环境,执行离线安装命令。第七步,装完以后用 pip list 核对一下关键包版本,然后跑项目验证。

这七步每一步都有对应的验证点,不要跳。尤其是第七步,很多人在这一步偷懒,结果上线后报错才回来看版本,反而更浪费时间。

4. 常见问题与排查技巧实录

4.1 Windows 下 pip freeze 出现“file:///”或“本地路径”开头的包名

这种情况我碰到过好几次。Windwos 上如果你用 pip 安装过本地 .whl 文件或者用了某些特殊的 index 地址,pip freeze 的输出可能会出现类似:

package @ file:///D:/downloads/some_package.whl

或者直接是本地路径。这种行写到 requirements.txt 里,换到别的机器上执行 pip install -r 就废了,因为它指向的是你本地磁盘路径。

解决办法是:要么手动把这一行改成正常的包名==版本号格式,要么干脆用 pipreqs 重新生成清单,让 pip 去查当前环境的版本信息,就不会有这个路径问题。踩过一次后我再没用纯 pip freeze 干过交付环境的活,基本都是 pipreqs 生成后人工补漏。

4.2 pipreqs 扫描不到动态导入的库

pipreqs 是静态扫描 import 语句的,如果你代码里用了 importlib.import_module() 动态加载模块,或者在一个字符串里拼模块名再导入,pipreqs 根本扫不到。我在一个数据采集项目里踩过这个坑,项目用 entry_points 插件机制动态加载了几个采集器,pipreqs 生成清单后什么都有,就是少了核心的采集框架库,等上了服务器才发现。

碰到这种项目,最可靠的方法是:先用 pipreqs 生成一份基础清单,然后看一遍代码里有没有 import 语句之外的可疑模块引用,有不确定的就主动加进 requirements.txt。宁可多装一两个无关包,也不能漏掉实际要用到的包。

4.3 导出 whl 时提示某个包只有源码包没有 wheel

这个问题我在 3.3 节已经提过,这里再说一个实际判断方式:当你看到下载日志里出现 Building wheel for xxx(pyproject.toml)而不是直接下载 whl 文件时,说明这个包在 PyPI 上提供的是源码包。如果你没有加 --only-binary=:all:,pip download 会把 sdist 源码包下载下来,后缀是 .tar.gz 而不是 .whl。这在离线环境里非常尴尬,因为目标机器如果没有编译工具链,源码包根本装不了。

最省事的应对方案:如果这个包有新版或老版提供了 whl,就尝试指定对应版本;如果没有,就只能接受目标机器上要装编译工具的事实,或者考虑换一个功能等价且有 wheel 的包。这种问题越早发现越好,别等到交付前一天才发现有包没法离线装。

4.4 不同 Python 版本导出的 whl 文件混用会报“not a supported wheel”

每台机器的 Python 版本可能不一样,3.9 和 3.10 的 ABI 接口有差异,很多 C 扩展的 whl 文件不能跨版本使用。比如你在 Python 3.9 环境里跑 pip download,拿到的 numpy whl 可能在 Python 3.10 的机器上装不了。

离线导出前,必须先确认目标机器的 Python 大版本。如果目标机器已经有 Python 3.10,那导出时在开发机上执行 pip download 之前,可以用 --python-version 310 参数来指定,这样 pip 会去找兼容 Python 3.10 的包,不管你当前执行命令的 Python 是哪个版本。当然,最稳妥的还是直接在同样版本的 Python 环境里去下载。

4.5 目标机器安装完成,import 依然报 ModuleNotFoundError

这个问题的排查思路要先看依赖树。有时候你确实把所有依赖都导出了,但某个包的某个功能需要额外安装一个子依赖,而这个子依赖只在运行时才被 import,pip download 自动解析依赖时可能没抓到。

排查方法是:在目标机器上打开 Python,执行 import xxx 看完整报错,它会提示缺哪个模块,然后回开发机用 pip download 下载这个缺失的模块,再传到目标机器装上。这种问题往往出现在一些功能比较复杂的库上,比如 psycopg2 在一些环境下还依赖 psycopg2-binary,或者 lxml 在某些版本下还依赖 html5lib。

4.6 常见问题速查表

问题原因解决办法
requirements.txt 里有 file:/// 开头的路径之前用本地 whl 安装过手动改版本号或用 pipreqs 重新生成
pipreqs 漏包动态导入没被静态扫描到人工检查 importlib 和字符串拼接导入
目标机器装不了某个 whl平台或 Python 版本不匹配检查文件名里的 cp310/manylinux 标记
离线安装时提示缺包依赖解析漏了子依赖回开发机补下对应 whl
pip download 提示只有 sdist包没有发布 wheel换版本或准备编译工具链
同一份 whl 放不同机器结果有的能装ABI 不兼容按目标机器的 Python 版本分别导出

5. 给新手的几条额外建议

我在实际项目里反复踩重复的坑,把几条最重要的心得说在前面。

第一,做依赖导出和离线包之前,务必先建一个全新的虚拟环境,在这个干净环境里把项目跑通,再导依赖。不然你的开发机上残留的那些无关包会混进清单里,导致离线包体积大、装起来也容易冲突。

第二,requirements.txt 和 whls 目录一定要一起打包交付,缺一个都不行。如果只给别人 requirements.txt,对方就还得联网;如果只给别人 whls 却没有清单,对方也不知道该装哪个版本。最好在交付文档里写清楚两条命令,一条是离线安装命令,一条是联网快速安装命令,让对方自己选。

第三,用 pip download 的时候,强烈建议加上 -i https://pypi.org/simple 或者你常用的国内镜像源,尤其在国内网络环境下,默认源下载速度可能无法接受。指定镜像源不会影响 whl 文件的内容,只影响下载速度。

pip download -r requirements.txt -d ./whls -i https://pypi.tuna.tsinghua.edu.cn/simple

第四,导出完以后建议顺手统计一下 whl 文件总数和总大小,心里有数。我之前交付过一个项目,requirements.txt 里只有十来行,结果 pip download 下来发现还带了一层很深的传递依赖,总共 40 多个 whl 文件,十几个 MB。你提前知道这些数据,在跟对方沟通交付内容时会更从容。

第五,把这套流程写成一个 shell 脚本或者 PowerShell 脚本,以后每次交付都直接跑一遍,比每次手动敲命令可靠得多。脚本的核心就三行:生成 requirements.txt、准备目录、执行 pip download,然后把 requirements.txt 复制进交付目录。我记得自己第一次手动操作时漏掉了其中一个子包,导致交付现场手忙脚乱,从那以后再没手动执行过。

6. 这个内容后续还能怎么扩展

如果你经常做离线部署,可以在这个基础上进一步研究离线安装私有 PyPI 源,比如用 devpi 或者 bandersnatch 搭一个本地镜像,这样团队内部的所有项目都能通过配置 pip.conf 指向内网源来安装依赖,不用每个人手动维护 whl 文件目录。

还可以把这一套流程和 Docker 结合起来,写一个 Dockerfile,在构建阶段把 requirements.txt 和 whls 目录打进镜像,配合多阶段构建把体积控制到最小。我之前负责的一个项目就是这么优化的,基础镜像只装 Python 运行环境,项目依赖用离线包一次性安装,镜像构建速度反而比在容器里现场 pip install 快不少。

如果你是用 Poetry 或者 uv 这些现代包管理工具,它们也都有导出 requirements.txt 的能力。Poetry 对应 poetry export -f requirements.txt -o requirements.txt,uv 对应 uv pip compile,底层语义和 pip-tools 类似。用它们管理依赖的项目,导出离线包的核心逻辑不变,只是前面生成清单的环节换成对应命令就行。

最后再分享一个小技巧:在导出 whl 文件以后,别急着删掉,留着在开发机上也是一个很好的“依赖缓存”。以后重建环境时,可以先用 --find-links=./whls 从本地装,装不上的再联网,能省不少时间。尤其是网络不稳定的情况下,这个本地目录就像个小型包仓库,整个开发体验会顺畅很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询