先把这个报错当成一次机会来看:它其实是 pip 在替你守门。
做 Python 项目交付的人,八成都在某个深夜或者某条 CI 流水线上见过这行刺眼的红字:
ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE.翻译成人话就是:pip 在安装依赖时,发现 requirements.txt 里锁定的哈希值,和它实际下载到的文件算出来的哈希对不上。听起来像个小问题,但它会让整个 pip install 直接失败,一个依赖都不装——CI 挂、容器构建挂、本地环境同步挂。我第一次遇到时,第一反应是"换个源重装",结果换完还报,后来才意识到根因完全不在源上。
这不是 pip 在闹脾气,恰恰相反,这是 pip 在正常工作。哈希校验是 pip 用来防止依赖被篡改的安全机制,报错恰恰说明它发现了一个"不该出现的变化"。把这套机制搞懂,你不仅能快速修复这条报错,还能顺手把项目的依赖管理做得更扎实。这篇文章适合所有用 Python 做开发、维护 CI/CD 流水线、或者负责应用打包交付的人,下面我把触发场景、根本原因、五种解决方案和一个完整的实战排查案例一次讲清楚。
1. 先搞清楚报错场景:哈希校验模式是怎么被触发的
1.1 三种最常见的触发方式
这条报错只在 pip 的哈希校验模式(hash-checking mode)下才会出现。你可以在三种情况下进入这个模式:
- requirements.txt 文件第一行或开头写了
--require-hashes; - requirements.txt 里,任意一个依赖行后面带了
--hash=sha256:xxxx这样的参数; - 命令行执行 pip install 时手动加了
--require-hashes。
注意一个容易忽略的规则:只要 requirements.txt 里出现了哪怕一个--hash,pip 就会自动进入哈希校验模式,不管你命令行有没有加--require-hashes。这是 pip 文档里的明确约定,很多人没意识到这一点,导致"我明明没要求校验哈希啊"的困惑。
一个典型的带哈希的 requirements.txt 长这样:
--require-hashes requests==2.31.0 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1这种文件通常不是手写的,而是用 pip-tools、hashin 这类工具自动生成的,目的就是锁定依赖的精确产物,保证每一次安装的内容完全一致。
1.2 完整报错信息逐行解析
完整的报错比标题那行长得多,信息量也大得多。我贴一段真实场景下的报错:
ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE. If you have updated the package versions, please update the hashes. Otherwise, examine the package contents carefully; someone may have tampered with them. requests from https://pypi.org/simple/requests/: Expected sha256 58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f Got sha256 942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1逐行拆解一下重点:
- 第一段话是 pip 的标准提示,前半句"如果你更新了包版本,请同步更新哈希"是针对最常见原因的,后半句"请仔细检查包内容,可能有人动手脚"是安全提示;
- 第二行
requests from https://pypi.org/simple/requests/告诉你是哪个包、从哪个源下载的; Expected sha256是 requirements.txt 里锁定的哈希;Got sha256是 pip 实际下载到的文件计算出的哈希。
Expected 和 Got 不一致,这就是全部矛盾所在。接下来要做的不是对着屏幕发愁,而是先判断一个问题:这两串哈希,谁是对的?
2. 为什么哈希会对不上:从根上排查
2.1 版本漂移:锁的是旧内容,源上已经是新文件
这是最普遍的原因。有人把依赖版本号写成requests==2.31.0,哈希也锁定了当时 PyPI 上 2.31.0 对应的文件。但某些镜像源或企业内部的私有源,可能缓存的构建产物和官方源不完全一致;或者某个包在同一个版本号下重新发布了新构建文件(这种情况在部分维护不规范的包身上真的发生过)。于是 pip 下载到的文件变了,哈希自然对不上。
遇到这种情况,一般重新生成一次哈希就能解决。但要注意,如果同一个版本号在源上确实换了产物,那意味着"同一版本号可能装到不同内容"这件事本身就值得警惕,说明这个源或这个包的发布流程不太靠谱。
2.2 镜像源不同:不同源提供的构建产物不一样
很多公司内部有 PyPI 镜像,或者开发者习惯用国内镜像加速。同一个包在官方 PyPI 上是 manylinux 的 wheel,在某个镜像上可能被同步成了源码包 sdist,或者因为同步延迟,镜像上还是旧版本的 wheel。文件不同,哈希就不同。
这就是为什么我在后面会特别强调:生成哈希的源,必须和实际安装用的源保持一致。否则你拿着一份在阿里云镜像上生成的哈希,去官方 PyPI 上安装,哈希对不上是必然事件。
2.3 平台差异:不同系统、不同 Python 版本的 wheel 天然不同
这一点是新手最容易踩的坑。py3-none-any这种纯 Python 通用 wheel 哈希全网一致,但很多带 C 扩展的包会根据平台提供不同的 wheel,例如:
- Windows 上有
win_amd64的 wheel; - macOS 有
macosx_10_9_x86_64的 wheel; - Linux 有
manylinux2014_x86_64的 wheel。
这些文件的哈希值是完全不同的。如果你的 requirements.txt 里只锁了 macOS 上生成的 wheel 哈希,换到 Linux CI 机器上安装,pip 会下载 Linux 的 wheel,哈希立刻对不上。同一份 requirements 不能在多个平台间这么硬搬。
2.4 本地缓存损坏:断网、磁盘 IO 异常导致的坏缓存
pip 默认会把下载的包缓存到本地,下次安装直接用缓存。如果缓存文件在写入时出了问题,比如磁盘满了、进程被杀、下载中断,缓存里的包可能是个损坏的文件。这时候 pip 拿着损坏文件和 requirements 里的哈希比对,结果自然是不匹配。
这种情况有一个明显特征:同一个报错在同一台机器上反复出现,但是在干净环境或别人机器上又正常。如果你发现"只有我的机器报错,CI 上明明好的",先怀疑缓存。
2.5 安全风险:源被篡改(概率低,但不能排除)
pip 报错提示"someone may have tampered with them"不是吓唬人。如果以上所有原因都排除了——版本没动、源没换、缓存清了、平台一致——哈希仍然对不上,那确实存在依赖被篡改的可能性。这在大型公共镜像被入侵的事件里真实发生过。
遇到这种情况,建议立刻做两件事:一是换官方源重新下载比对哈希;二是核对一下这个包的发布者信息和发布时间是否正常。大多数时候是虚惊一场,但这个检查动作不能省。
下面把常见原因和排查方向整理成一张速查表,方便你对照:
| 可能原因 | 典型特征 | 排查方向 |
|---|---|---|
| 版本漂移 / 源上产物更新 | 同一版本号下哈希变了 | 重新生成哈希 |
| 镜像源不同 | 换了源之后开始报错 | 固定源并重新生成哈希 |
| 平台 / 系统差异 | requirements 在别的机器上生成的 | 按目标平台重新生成哈希 |
| 本地缓存损坏 | 只有本机报错,干净环境正常 | 清缓存重试 |
| 依赖被篡改 | 所有常规原因排除后仍报错 | 换官方源核对,谨慎处理 |
3. 解决方案实操:从最推荐到应急
3.1 方案一:重新生成哈希并更新 requirements.txt(最推荐)
这是最正规、也是唯一值得长期使用的解法。核心思路是:让 pip 从你指定的源下载正确的包文件,计算出新哈希,再写回 requirements.txt。
第一步,下载包文件但不安装:
pip download requests==2.31.0 -d /tmp/pkg --no-deps第二步,用 pip 自带命令计算哈希:
pip hash /tmp/pkg/requests-2.31.0-py3-none-any.whl输出会是这样:
/tmp/pkg/requests-2.31.0-py3-none-any.whl: --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1把输出的--hash=...复制到 requirements.txt 对应依赖行后替换掉旧哈希即可。
如果依赖很多,逐个手动替换不现实,推荐用 hashin 这个工具自动完成:
pip install hashin hashin requests==2.31.0 -r requirements.txthashin 会从 PyPI 拉取当前版本的所有可用文件,把全部平台的哈希都写进去,同时自动把旧哈希替换掉。生成的 requirements 在不同平台上安装时都能匹配到对应的哈希,省心很多。
如果你原本就是用 pip-tools 管理依赖,更简单的做法是直接用 pip-compile 重新生成整个文件:
pip install pip-tools pip-compile --generate-hashes requirements.in -o requirements.txtrequirements.in 里只写顶层依赖,pip-compile 会自动解析出完整依赖树并生成带哈希的 requirements.txt。这个方案最大的好处是:哈希永远和当前锁定的版本一一对应,不会出现"版本升了哈希忘更"的问题。
3.2 方案二:临时绕过哈希校验(只用于排查定位)
有时候你只是想快速确认"是不是哈希的问题",而不是真的想绕过安全机制。这时候可以临时去掉校验,让安装先跑通,把问题定位出来。
最稳妥的临时做法是复制一份 requirements 文件,把里面所有的--hash=xxx和开头的--require-hashes删掉,再用这个临时文件安装:
cp requirements.txt requirements_nohash.txt sed -i 's/ \\//; s/--hash=sha256:[a-f0-9]*//g' requirements_nohash.txt pip install -r requirements_nohash.txt注意,pip 在安装时要求所有依赖行格式正确,你手动删哈希时要小心不要把换行结构弄坏。如果文件是带反斜杠续行的格式,像我上面 sed 命令里处理的那样,要先处理续行符再处理哈希字段。
这里必须强调:这个方案只适合本地快速验证,千万不要在 CI 流水线或者生产环境这么干。哈希校验是依赖供应链安全的重要防线,删掉它等于把门锁拆了。我见过有人图省事直接在 Dockerfile 里加跳过校验的参数,后来团队查一个诡异的环境问题时才发现是依赖被污染了,那叫一个后悔。
3.3 方案三:清理 pip 缓存后重试
如果你判断报错和缓存损坏有关,先看缓存,再清理。
查看缓存现状:
pip cache info pip cache list | grep requests只清理某个包:
pip cache remove requests或者干脆全部清掉:
pip cache purge如果你的 pip 版本比较老(20.1 之前没有 cache 子命令),直接加--no-cache-dir绕开缓存安装一次,也能达到同样效果:
pip install -r requirements.txt --require-hashes --no-cache-dir每次安装都--no-cache-dir不推荐,但作为一次性排查手段非常有效。如果清了缓存后问题消失,基本可以确认是缓存文件损坏。
3.4 方案四:固定镜像源并保持哈希一致
既然哈希和源强相关,那就把源彻底固定下来。常见做法是在 pip 配置里写死 index-url,比如在pip.conf(Linux/macOS)或pip.ini(Windows)中配置:
[global] index-url = https://mirrors.aliyun.com/pypi/simple/ trusted-host = mirrors.aliyun.com配置好后有两个动作要做:一是所有生成哈希的操作都要基于这个源,二是同一份 requirements 在所有使用方那里都用同一个源。只要源统一了,哈希不一致的概率会大幅降低。
顺便提醒一个实际操作细节:如果你用官方 PyPI 生成哈希,但又用内网镜像安装,即使镜像内容同步得很完整,也可能因为同步时间差导致临时性不一致。这时候不要急着改哈希,先确认镜像是否已经同步到你锁定的版本文件。
3.5 方案五:改用锁文件方案,从源头避免反复踩坑
哈希报错反复出现,本质上是"手工维护哈希"这件事太容易出错。更彻底的解决办法是把哈希生成交给工具自动管理。
两个主流选择:
- pip-tools:用 requirements.in 声明依赖,pip-compile 生成带哈希的 requirements.txt,升级依赖时重新执行一次编译即可;
- poetry:用 pyproject.toml 声明依赖,poetry.lock 自动记录每个依赖的精确版本和哈希,团队直接基于 poetry.lock 安装。
这两个工具我都深度用过,个人体会是:pip-tools 更贴近传统 pip 工作流,迁移成本低;poetry 功能更全面,但如果你只是想要哈希锁定,pip-tools 就够用了。关键收获是,引入锁文件工具后,我再也没遇到过"哈希忘更新"这类人为失误。
4. 实战复盘:一次 CI 构建失败的完整排查过程
理论讲再多,不如走一遍真实排查流程。下面是我最近处理过的一个典型案例,按时间线完整记录。
4.1 问题复现与日志收集
项目是一个内部 API 服务,CI 流水线第一步就是创建虚拟环境并安装依赖。某天构建开始报错,日志尾部是这样的:
Collecting requests==2.31.0 Downloading requests-2.31.0-py3-none-any.whl (62 kB) ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE. requests from https://mirrors.internal.example.com/pypi/web/simple/requests/: Expected sha256 58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f Got sha256 942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1第一件事不是改代码,而是把关键信息记录下来:报错包名、expected 哈希、got 哈希、下载源地址。这些信息后面每一步排查都会用到。
4.2 定性:用排除法缩小原因范围
我按这个顺序过了一遍:
- 确认 requirements.txt 最近有没有人动过。查 git 记录,发现这份 requirements 是三个月前用 hashin 生成的,期间没人改过;
- 确认版本有没有漂移。
requests==2.31.0是写死的,PyPI 上这个版本也没有重新发布过新构建; - 确认平台有没有变。CI 一直是同一个 Ubuntu runner,平台没变;
- 检查缓存。CI 是每次全新环境,不涉及本地缓存;
- 怀疑点落在了镜像源上。
进一步看日志里的下载地址mirrors.internal.example.com,这是公司内部镜像。我顺手对比了一下:用同样的版本在官方 PyPI 上手动下载,哈希和 requirements 里锁定的完全一致。也就是说,requirements 没问题,问题出在内部镜像提供的文件。
最后查到的原因是:运维那边升级了内部镜像的存储后端,重新同步了一批元数据,可能拉取到了一份缓存中残留的旧构建产物。镜像上的文件和官方源不一致,哈希自然对不上。
4.3 生成正确哈希并修复
原因定性之后,修复方案很明确:以内部镜像为准,重新生成哈希并更新 requirements,同时向运维反馈镜像异常。
我先把包从内部镜像下载下来:
pip download requests==2.31.0 -d /tmp/pkg --no-deps -i https://mirrors.internal.example.com/pypi/simple/然后用 sha256sum 做了一次独立验证:
sha256sum /tmp/pkg/requests-2.31.0-py3-none-any.whl结果和报错信息里的 Got 值一致,确认这个文件就是当前镜像上真实存在的产物。
接着用 pip hash 生成新哈希:
pip hash /tmp/pkg/requests-2.31.0-py3-none-any.whl把新哈希替换进 requirements.txt 后,再次触发 CI 构建,安装阶段顺利通过。
4.4 验证与后续加固
修复完成只是第一步。为了不让同类问题再次卡住构建,我做了三件加固的事:
- 在 CI 里加了构建缓存清理步骤,确保每次安装都从镜像拉取最新文件;
- 联系运维核对镜像同步机制,确认后续不会再出现元数据和实际文件不一致的情况;
- 在团队文档里明确写了一条规范:任何依赖的哈希变更,必须记录变更原因,不能只改文件不留说明。
这次排查给我最大的教训是:哈希对不上,先不要急着"重新生成哈希一把梭",因为这个动作会把真正的异常掩盖掉。先判断哈希不一致是正常变更还是异常变更,再去改文件,顺序不能反。
5. 常见问题速查与避坑清单
5.1 常见问题速查表
| 现象 | 可能原因 | 首选处理方式 |
|---|---|---|
| 换了个机器就报错 | 平台不同,wheel 哈希不同 | 用 hashin 补全所有平台哈希 |
| 同一台机器突然报错 | 镜像源内容更新 / 缓存损坏 | 先清缓存,再检查镜像源 |
| 升级依赖后报错 | 版本更新但哈希没更新 | 重新生成哈希 |
| 公司镜像源报错 | 镜像同步异常或内容不一致 | 核对官方源哈希,反馈给运维 |
| 只改了一个包却全盘报错 | 可能依赖树里有间接依赖哈希过期 | 用 pip-compile 重新生成整个文件 |
5.2 新手最容易踩的几个坑
第一个坑:修改 requirements.txt 时把续行符和哈希格式弄坏。带哈希的文件每行依赖后面通常有反斜杠续行,手改时少一个空格或者换行符位置不对,pip 会报"解析 requirements 文件失败"之类的错误。这种问题比哈希不匹配还难排查,因为报错信息指不到具体行。所以能用工具生成就别手改。
第二个坑:只锁一个平台的哈希,然后到处复制这份文件。前面说过,不同平台 wheel 哈希不同,正确的做法是用 hashin 生成包含所有平台哈希的文件,或者在每个目标平台上分别生成。
第三个坑:遇到报错就加--no-deps或者直接删掉依赖。这种"解决不了问题就解决提出问题的人"的思路,短期能绕过去,长期会把依赖关系搞乱。哈希校验绕过去之后,你等于放弃了对供应链安全的检查,这在企业级项目里是不能接受的。
第四个坑:把--require-hashes模式和普通锁定版本混为一谈。锁定版本号只能保证版本一致,不能保证文件内容一致;哈希才能精确到文件级别。如果你的项目对安全要求高,必须用哈希,不能只锁版本号。
5.3 我在实际使用中的一些习惯
踩过几次坑之后,我现在形成了几个固定习惯,分享给你参考。
生成哈希统一用一个源。我通常直接固定官方 PyPI 或者公司唯一指定的内部镜像,生成哈希和实际安装必须走同一个源,这一点写进了团队的流水线模板。
升级依赖后必重新编译锁文件。不管是用 hashin 还是 pip-compile,升级任何依赖之后都要重新生成哈希,并且把新旧哈希变更提交到 MR 里一起 review。这样每次哈希变化都是可见的、可追溯的。
CI 里加一道哈希校验的确认步骤。具体做法是安装时不加--no-cache-dir,但定期全量清理一次缓存。这样既能保证安装速度,又能避免缓存垃圾导致偶发的哈希问题。
如果有一天你排查到深夜实在没思路,记住一个最简单的自检顺序:先确认 requirements 文件本身有没有被人改过,再确认包在源上是否还是原来那个文件,最后清一次缓存重试。八成的问题都能在这三步之内找到答案。剩下两成,基本就是源的问题,找运维核对就好,不用自己硬扛。