拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

解决pip哈希校验失败:从报错到Python依赖管理加固

解决pip哈希校验失败:从报错到Python依赖管理加固

先把这个报错当成一次机会来看:它其实是 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.txt

hashin 会从 PyPI 拉取当前版本的所有可用文件,把全部平台的哈希都写进去,同时自动把旧哈希替换掉。生成的 requirements 在不同平台上安装时都能匹配到对应的哈希,省心很多。

如果你原本就是用 pip-tools 管理依赖,更简单的做法是直接用 pip-compile 重新生成整个文件:

pip install pip-tools pip-compile --generate-hashes requirements.in -o requirements.txt

requirements.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 定性:用排除法缩小原因范围

我按这个顺序过了一遍:

  1. 确认 requirements.txt 最近有没有人动过。查 git 记录,发现这份 requirements 是三个月前用 hashin 生成的,期间没人改过;
  2. 确认版本有没有漂移。requests==2.31.0是写死的,PyPI 上这个版本也没有重新发布过新构建;
  3. 确认平台有没有变。CI 一直是同一个 Ubuntu runner,平台没变;
  4. 检查缓存。CI 是每次全新环境,不涉及本地缓存;
  5. 怀疑点落在了镜像源上。

进一步看日志里的下载地址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 文件本身有没有被人改过,再确认包在源上是否还是原来那个文件,最后清一次缓存重试。八成的问题都能在这三步之内找到答案。剩下两成,基本就是源的问题,找运维核对就好,不用自己硬扛。

返回列表