直接接触这类需求的朋友应该都知道,CentOS 7 服务器最常见的宿命就是窝在内网:不能上外网、不能换 yum 源、连 pip 默认的 PyPI 都连不上。偏偏业务系统还需要在 Python 环境里调用 HTTP 接口,requests这种几乎人手一个的库就成了第一块拦路石。标题里这十几个字——“Centos 7 离线安装 requests 库”——背后其实藏着一整套经验:怎么在有网的机器上准备安装包、怎么把依赖关系一次性捋顺、装到一半报错要怎么定位、后续批量部署怎么不重蹈覆辙。这篇文章就把这些坑和路子都摊开讲清楚,适合正在做内网实施、或者第一次给离线服务器装 Python 组件的新手参考。
1. 离线安装翻车清单:依赖链、abi标识和Python版本三座大山
很多人第一次干这活儿,觉得“离线安装不就是把包拷过去 pip install 吗”。真上手才发现,问题远没有这么简单。requests不是孤零零一个文件,它背后挂着一串依赖:urllib3、certifi、charset_normalizer、idna,缺一个都不行。而且这些依赖还有各自的下游依赖,比如urllib3在某些版本里又需要brotli之类的可选扩展。如果只把requests主包拷过去,pip install的时候照样会报No matching distribution found。
1.1 依赖链:不只是 requests 一个包的事
先给新朋友梳理一下requests的依赖关系。以当前还在用的requests 2.31.0为例,它会依赖:
urllib3 <3,>=1.21.1:负责底层连接池和 HTTP 协议逻辑,这是最核心的重型依赖certifi>=2017.4.17:提供根证书库,HTTPS 请求要验证证书就靠它charset_normalizer>=2,<4:处理响应内容的字符编码检测idna<4,>=2.5:处理国际化域名和 URL 编码
这里有一个非常要命的坑:urllib3从 2.0 开始要求 OpenSSL 1.1.1 以上,而 CentOS 7 自带的系统 OpenSSL 是 1.0.2k 版本。如果你在离线机器上装了最新版urllib3 2.x,运行时大概率会遇到SSLError或者导入报错。所以离线环境下给 CentOS 7 装requests,第一件事不是装最新,而是把版本锁在兼容区间——urllib3老老实实用1.26.x,requests用2.31.x或更早,这套组合我实测是最稳的。
1.2 ABI 标签:为什么源码包在离线机器上编译不过去
pip下载安装包时其实分两类:一类是tar.gz源码包,另一类是编译好的wheel包。源码包装到目标机器时要现场编译,而内网服务器往往没有完整的编译工具链——gcc、make、python3-devel等头文件可能一个都不齐。结果就是pip install开始编译,最后报一个Unable to find vcvarsall.bat或command 'gcc' failed之类的错。
wheel 包则不同,它是预先编译好的二进制,里面带着平台和 Python 版本的标识,比如requests-2.31.0-py3-none-any.whl,这个py3-none-any意味着它是纯 Python 代码、不依赖具体平台。但urllib3这种带 C 扩展的可不一定了,它的 wheel 会有cp36-cp36m-manylinux2014_x86_64这样的 ABI 标签,表示这个包只能在 Python 3.6 的 CPython 解释器、x86_64 架构下用。离线安装时如果下载的 wheel 与目标机器 Python 版本不匹配,pip会直接忽略掉这个文件,然后报找不到合适的发行版。
1.3 Python 版本选择:CentOS 7 自带的 2.7 和项目需要的 3.x
CentOS 7 系统自带的是 Python 2.7.5,但现在业务代码基本都跑在 Python 3 上。在离线环境里你最好先想清楚目标机器上到底装了什么 Python。如果是自己编译的 Python 3.6,那就要匹配cp36-cp36m;如果是 Python 3.8,那就是cp38-cp38。而且要注意,从 Python 3.9 开始不再生成manylinux2014的 32 位包,现代很多 wheel 标签都是manylinux_2_17_x86_64。如果你的 CentOS 7 是 32 位系统,或者内核版本比较老,下载包的时候就得手动指定兼容的 platform 标签,否则装了也调用不起来。
提示:装机之前先跑一句
python3 -c "import sys; print(sys.version)"和python3 -c "import platform; print(platform.platform())",把版本记下来,后面所有选包动作都要围绕这三个信息展开。
2. 出发之前:在有网机器上准备离线安装包的正确姿势
既然目标机器碰不到外网,那就得换一台同样架构、同样系统版本、能上网的机器来做“打包动作”。这一步看着简单,但很多人栽就栽在:直接在解压出来的site-packages目录里把整个文件夹拷贝过去。这么做不是完全不行,而是你根本搞不清楚哪些文件是系统自带的、哪些是请求库带来的,后期清理升级全是隐患。正规做法是用pip download把安装包按依赖关系全部拉下来,拉到本地一个文件夹里,再整体搬到内网机器上。
2.1 用 pip download 一次性拉全所有依赖
在能上网的机器上,先建一个干净的目录,然后执行:
mkdir -p /tmp/requests_offline cd /tmp/requests_offline pip download requests==2.31.0 \ --dest /tmp/requests_offline \ --only-binary=:all: \ --platform manylinux2014_x86_64 \ --python-version 36 \ --implementation cp \ --abi cp36m这段命令里几个参数非常关键,逐一说下:
--only-binary=:all::强制只下载编译好的 wheel,不碰源码包。这样能避免把tar.gz拉下来,减少目标机器编译失败的几率。--platform manylinux2014_x86_64:指定目标平台。manylinux2014是当前兼容性最好的 Linux 平台规范,CentOS 7 完全满足。如果你的目标机器是 64 位,就用x86_64,别省这一步,否则 pip 不会帮你智能匹配。--python-version 36:对应cp36,说明目标解释器是 Python 3.6。如果目标机器装的是 3.8,就改成38。--abi cp36m:ABI 标识。这个要和--python-version配对使用。cp36m的意思就是 CPython 3.6 的m前缀(启用 pymalloc 的版本),Python 3.8+ 已经不再区分m了,所以 3.8 对应的是cp38。
拉完之后,/tmp/requests_offline目录下会有四个以上的 wheel 文件。你可以用ls -l看一眼,确认里面是否有requests、urllib3、certifi、charset_normalizer、idna等文件。如果用pip download时报错说某个包没有匹配的 wheel,多半是你指定的平台/ABI 组合太严格,可以适当放宽--platform为manylinux1_x86_64试试,因为manylinux2014向上兼容manylinux1。
2.2 如果只有源码包:优先换个版本,而不是硬着头皮编译
有些包在新版本里只提供源码包,比如某些更新较慢的项目。这时候我的建议是:不要硬编译,先试着降低一个版本号,看老版本有没有现成的 wheel。因为离线机器上没有编译链的风险太高,一个all:all强制过滤就能帮你挡住大部分坑。如果实在全部都是源码包,那就退而求其次,在打包机器上把gcc和python3-devel装好,提前在打包机上编译好再拷贝,但这就绕回了“复制 site-packages”的老路,尽量少用。
2.3 别忘了检查 pip 自身版本
这里有个隐藏坑:CentOS 7 如果用系统自带的 pip(老版本如 8.x、9.x),它不支持--platform、--only-binary这类参数。所以在有网打包机上,先升级一下 pip 再执行下载命令:
python3 -m pip install --upgrade pip升级完 pip 之后,download命令的参数行为才稳定。而且后续在内网安装机上,如果它的 pip 版本太老,也可能不认--no-index这类参数,我在第 3 节里单独说。
3. 在离线的 CentOS 7 上执行本机安装:--no-index 与 --find-links 的真正用法
安装包准备齐全之后,怎么把这些 wheel 拷到目标机器上?最常用的办法是 U 盘拷、内网共享目录,或者直接 scp 过去(前提是内网对某些端口有放行)。放在哪个目录都行,我这里用一个约定俗成的路径/opt/offline_packages,里面放着所有.whl文件。
3.1 安装指令:连 pip 的默认源一起关掉
目标机器上执行:
python3 -m pip install --no-index --find-links=/opt/offline_packages requests==2.31.0这条命令的意思很直白:不联网、不去 PyPI 找包,只从本地指定目录里查找安装包。--no-index是彻底断了 pip 访问外网的念头,--find-links告诉它本地备选目录在哪。执行时大概率会遇到三种情况:
- 顺利装完,exit code 0,这种当然最好;
- 提示找不到
requests的匹配发行版,说明你目录名写错了、或者 wheel 不在这目录里; - 提示某个依赖不满足,比如缺
urllib3或者charset_normalizer,说明打包时依赖没拉全。
3.2 装完之后立刻验证 import
别急着收工,马上在目标机器上验证一遍:
python3 -c "import requests; print(requests.__version__)"如果这一步能正常输出版本号,说明基础安装没问题。但这只是“导入不报错”,HTTPS 请求能不能用、证书路径对不对,还要下一步实测。我在日常实施里会写一个极小的连通性测试:
python3 - <<'EOF' import requests try: r = requests.get("http://127.0.0.1:8888/test", timeout=5) print("HTTP OK", r.status_code) except Exception as e: print("HTTP FAIL", repr(e)) EOF如果你在内网根本没有一个现成的 HTTP 服务,可以用python3 -m http.server 8888临时起一个,测完就关。测试走的是本机回环地址,不依赖外网,所以这台离线机器也能验证请求库的核心功能。
3.3 为什么我不建议直接用 pip install requests 然后指望它“离线命中缓存”
有人图省事,跑到内网机器上直接敲pip install requests,心想“也许系统缓存里有”。实际上 pip 的本地缓存不会自动存那些从未下载过的包,这么做只会卡在连接超时上,最后报Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None))这种错误。离线机器的正确逻辑永远是:先在外网把东西备好,再在内网用--no-index强制关闭网络路径,避免 pip 浪费时间反复重试。如果哪一天在内网机器上误操作,发现 pip 在反复重试,直接按Ctrl+C停掉,检查命令里是否漏了--no-index。
4. 安装失败的完整排查链路:从“找不到依赖”到“编译报错”的定位过程
这一节重点说问题定位,因为离线安装一旦失败,错误信息往往不会特别友好。我把这几年遇到的典型失败场景按顺序写出来,你可以照着这个链路排查。
4.1 第一类:pip 提示 No matching distribution found for requests
这是最常见的报错。排查顺序:
- 先确认目标机器的 Python 版本。如果目标是 Python 3.6,但你下载时指定了
--python-version 38,那requests的 wheel 确实是兼容的(py3-none-any),但urllib3可能只有cp38的 wheel,导致整体检索失败; - 再确认下载目录下是否有
.whl文件,用find /opt/offline_packages -name "*.whl"快速看一遍,确认文件是不是真的拷进去了; - 最后看文件名中的平台标签。比如你把
manylinux2014_x86_64的包放到 32 位机器上,pip 会直接忽略,报的还是“找不到”。
用一句话总结:报“找不到”不一定是包没给够,而是包和当前环境的标签不匹配。pip 在检索时是严格按环境标签过滤的,不是所有 wheel 都会进入候选列表。
4.2 第二类:ERROR: Could not find a version that satisfies the requirement charset_normalizer
这种一般出现在下载打包时拉漏了依赖。因为requests依赖charset_normalizer,而charset_normalizer在新版本里也可能依赖别的包(虽然目前还算干净)。为了保险起见,你可以在有网打包机上用这条命令强制把依赖树都拉全:
pip download requests==2.31.0 \ --dest /tmp/requests_offline \ --only-binary=:all: \ --platform manylinux2014_x86_64 \ --python-version 36 \ --implementation cp \ --abi cp36m \ --no-deps等等,--no-deps不是不要依赖吗?我这条命令是错的示范……把依赖拉全的命令应该是去掉--no-deps,它是默认行为,不加就是拉全。正确写法就是不加这个参数,直接跑一遍,pip 会自动解析整个依赖树。如果某些间接依赖没有被解析出来,可以手动补:
pip download urllib3==1.26.18 certifi==2023.7.22 charset_normalizer==3.2.0 idna==3.4 \ --dest /tmp/requests_offline \ --only-binary=:all: \ --platform manylinux2014_x86_64 \ --python-version 36 \ --implementation cp \ --abi cp36m手动指定这四个下游依赖,是防止 pip 依赖解析出现遗漏时最稳妥的兜底方案。尤其当你锁定requests 2.31.0时,urllib3建议固定到1.26.18,不要用 2.x。
4.3 第三类:编译时报错 gcc: command not found
如果你在下载时没有强制--only-binary,导致拉回来一个tar.gz,那目标机器上就会触发源码编译。CentOS 7 精简安装经常不带gcc,于是报gcc: command not found。内网又装不了编译链,这就非常被动了。所以前面说的--only-binary=:all:是保命参数,一定不要去掉。万一真的只有源码包,你要评估是否必须用这个版本,能换 wheel 版本还是换掉更好。千万别想着“先装上 gcc 再说”,内网环境的 yum 源可能同样不通,装了也白装。
4.4 第四类:运行报错 urllib3 v2.0 only supports OpenSSL 1.1.1+
这是最隐蔽的一类——安装过程没报错,导入也没报错,但真正发起 HTTPS 请求时突然炸了。原因就是urllib3 2.x对 OpenSSL 版本有硬性要求,CentOS 7 的系统 OpenSSL 太老,满足不了。解决办法是回到打包阶段,把urllib3锁到1.26.x,锁住之后requests调用的底层加密库行为会和系统 OpenSSL 兼容。这个坑我已经见了好几次,有些朋友把requests装好之后信心满满,一调外部接口立刻SSL: CERTIFICATE_VERIFY_FAILED或者直接段错误,浪费半天时间排查。所以版本锁定这个步骤千万别省。
4.5 一张表总结失败场景和处置方式
| 报错特征 | 根本原因 | 处理方案 |
|---|---|---|
| No matching distribution found | wheel 标签与环境不匹配 / 目录里没有包 | 核对 Python 版本和 ABI 标签,用find确认文件 |
| Could not find a version that satisfies ... | 依赖解析不完整 | 手动补齐四个核心依赖 |
| gcc: command not found | 下载了源码包触发了编译 | 重新下载时加--only-binary=:all: |
| urllib3 v2.0 only supports OpenSSL 1.1.1+ | urllib3 版本过新 | 锁定urllib3==1.26.18 |
| SSL: CERTIFICATE_VERIFY_FAILED | CA 路径缺失或证书库不匹配 | 重装certifi,或设置REQUESTS_CA_BUNDLE环境变量 |
5. 从单机到批量:虚拟环境迁移和本地软件源两个进阶玩法
如果你只是给一台机器装requests,做到第 3 节就可以收工了。但实际工作中往往是一批服务器,几十台 CentOS 7 都要配同样环境。这时候逐台执行 pip install 不是不行,但效率太低,而且每台机器可能历史残留不同版本的库,容易埋雷。我常用两个思路来解决批量问题。
5.1 思路一:虚拟环境整体迁移
在有网打包机上先把虚拟环境建好、装好requests,然后把整个虚拟环境目录打包拷到目标机器。具体步骤:
python3 -m venv /opt/venv_requests /opt/venv_requests/bin/pip install requests==2.31.0 cd /opt tar czf venv_requests.tar.gz venv_requests把venv_requests.tar.gz拷到目标机器,解压到同路径/opt/venv_requests。这里有个关键细节:虚拟环境里的脚本硬编码了解释器路径,所以确保目标机器解压路径与打包路径一致。也就是说,打包时在/opt下建的虚拟环境,解压也要放在/opt/venv_requests,不然python解释器头部的/opt/venv_requests/bin/python3就会失效。如果路径实在对不上,可以手动改一下bin/activate和脚本里的 shebang,但尽量别给自己找这个麻烦。
这种方式的优点是:验一次,批量复制,所有机器环境完全一致,不会出现 A 机器装了 urllib3 1.26、B 机器装了 urllib3 2.0 这类分叉。缺点是虚拟环境体积稍大(一般几十 MB),但在内网拷贝这点开销完全可接受。
5.2 思路二:打造一个本地软件源
如果后续还要装其他 Python 包,或者团队持续迭代需要频繁更新依赖,靠“拉一包、传一包”太死板。我当时在一个项目里搭过一个极简内网源:在一台能访问外网的机器上定期执行pip download拉下全量包,放到一个通过 HTTP 共享的目录,内网机器上设置:
python3 -m pip install requests==2.31.0 \ --index-url http://内网源IP:8080/simple/ \ --trusted-host 内网源IP这个方案的原理是用--index-url指向一个本地 HTTP 服务,只要内网能访问到这个地址,pip 就会把它当成 PyPI 来用。--trusted-host是因为内网源多半是 HTTP 明文,没有 HTTPS 证书,pip 默认会拒绝不安全的源。
其实你不用专门搭系统服务,用一条命令就能把目录变成源:
cd /opt/offline_packages python3 -m http.server 8080 --bind 0.0.0.0有人可能觉得这太简陋了,但这确实能解燃眉之急。前提是目录下需要有 pip 要求的目录结构,严格说pip按--find-links方式访问 HTTP 目录时不会强制/simple/结构,而按--index-url方式访问时必须有一个符合 PEP 503 的页面。所以更稳妥的做法是仍然用--no-index --find-links=http://内网源IP:8080/的方式,让 pip 直接扫描目录下面的 wheel。需要控制的只是把/opt/offline_packages作为 HTTP 根目录即可。配合这个方式,每台机器上的安装命令就变成:
python3 -m pip install --no-index \ --find-links=http://内网源IP:8080/ \ requests==2.31.0这样连 U 盘拷贝都省了,几十台机器可以并行装,效率提升非常明显。
6. 装完不是终点:验证 requests 可用性与依赖锁固化
装完、测完、批量部署完,事情还没结束。离线环境最怕的就是“这台机器能跑,那台机器启动就报错”。所以整个流程的最后一步,我通常会把验证脚本和依赖清单一并固化下来,方便后续维护。
6.1 一个标准的验证脚本
我在每个目标机器上都会留一个check_requests.py,内容很简单:
import ssl import sys import requests print("Python:", sys.version) print("requests:", requests.__version__) print("urllib3:", requests.packages.urllib3.__version__) print("OpenSSL:", ssl.OPENSSL_VERSION) try: r = requests.get("http://127.0.0.1:8888/test", timeout=5) print("HTTP test:", r.status_code) except Exception as exc: print("HTTP test failed:", repr(exc))这个脚本重点看三样东西:Python 版本、urllib3 版本、OpenSSL 版本。如果 urllib3 是 1.26.x,系统 OpenSSL 是 1.0.2k,HTTPS 基本不会出大问题。如果 urllib3 飙到 2.x,那你得警惕是不是有人后续手滑升级了。
6.2 用 requirements.txt 锁死版本
在离线目录里放一份requirements.txt,内容是:
requests==2.31.0 urllib3==1.26.18 charset_normalizer==3.2.0 idna==3.4 certifi==2023.7.22这样后面任何一台机器要重建环境,直接用--find-links加-r requirements.txt就能精确复现:
python3 -m pip install --no-index \ --find-links=/opt/offline_packages \ -r requirements.txt依赖被锁死的意义在于:离线环境本来就没有外网,一旦出了问题你很难临时拉新包。药不能乱吃,版本更不能乱跳。
6.3 我的一点体会
这套离线安装流程走下来,最核心的体会就是“准备阶段的严谨程度决定安装阶段的顺利程度”。大部分翻车事故都不是装的时候才发生的,而是打包的时候没考虑 ABI、没锁版本、或者贪图方便把源码包当成万能药。你只要始终记住三点:用 wheel 不用源码、用--no-index断网安装、把 urllib3 按死在 1.26.x,CentOS 7 离线装requests基本不会再有惊喜。最后再说个小技巧:打包目录命名时把 Python 版本和平台写进去,比如packages_py36_x86_64,这样过了一个月你自己回来看目录,也不会忘记这包是给谁用的。