☰
pip查看Python包所有已发行版本号:完整路线与避坑
2026/11/3 6:43:13 网站建设 项目流程

1. 版本号到底藏在哪:先建立一张地图

前阵子帮人排查一个老服务的依赖回滚问题,对方的需求很朴素:想知道某个包在 2022 年上半年到底发过哪些版本,好把依赖锁回去。他敲了pip index versions,屏幕上只出来一串最新的版本号,翻上去看历史记录也找不到更多。问题不在于命令敲错了,而在于很多人对“pip 查看 Python 某个包已发行所有版本号”这件事有一个误解——以为 pip 手里有一份完整的版本目录,随时能调出来。事实是 pip 自己不存任何版本清单,它每次都是现场去包索引上抓数据、自己解析、再按当前环境做过滤,最后吐给你一份“这份环境下看起来能装的版本”。你看到的是筛选结果,不是全量清单。

这个差别在查新包的时候几乎察觉不到,因为新包版本少、兼容性好、也没被撤过。一旦碰上迭代了七八年、发过上百个版本、中间还撤过几个版本的老包,或者你要查的是公司内部源上的私有包,pip 默认给你的那份列表就可能缺斤少两。所以这篇我想把这件事拆开讲:pip 查看 Python 某个包已发行所有版本号,到底有几条路,每条路的边界在哪,什么场景该用哪条,以及我这些年踩过的、文档里基本不会写的坑。

适合谁看?三种人。第一种是只想快速知道“这个包都有哪些版本”的普通使用者,看第二节就够了;第二种是要写脚本、做依赖审计、做版本监控的工程师,第三、四节是重点;第三种是维护内部私有源、经常被同事问“为什么我这里看不到某个版本”的人,第五节的排查表能省你不少时间。

1.1 版本信息在源上到底以什么形态存在

要搞清楚查询方法,先得知道数据长什么样。公共 PyPI 和企业内部制品库对外暴露的接口,本质上就两类。

第一类是Simple 索引,也就是 PEP 503 定义的那个/simple/<包名>/路径。它返回的是一个 HTML 页面,页面里密密麻麻全是链接,每个链接指向一个具体文件——requests-2.31.0-py3-none-any.whl、requests-2.31.0.tar.gz这样。注意它的粒度是文件,不是版本。同一个版本可能对应十几个文件(不同 Python 版本、不同平台的 wheel 加一个源码包),也可能一个文件都没有(被人手动清理过)。PEP 691 后来又给同一个路径加了 JSON 表示,只要在请求头里声明Accept: application/vnd.pypi.simple.v1+json就能拿到结构化数据,不用再正则抠 HTML。

第二类是JSON API,路径是/pypi/<包名>/json。它的粒度是发布(release),返回体里有个releases字典,键就是版本号字符串,值是该版本下的文件列表。这个视角更贴近人脑里的“版本”概念,但也正因为它是另一套逻辑,它和 Simple 索引的结果并不总是一致。

pip index versions走的是第一条路,读 Simple 索引然后自己聚合;绝大多数第三方查询工具走的也是这条路。理解这一点,后面很多“为什么我看到的和网页上不一样”的问题就有解了。

1.2 三条路线怎么选,先回答三个问题

我把选型判断压成三个问题,你按顺序过一遍,基本不会选错。

问题一:这个包在公网还是在内部源?公网包随便查,内部包的版本可能压根没同步到公网,也可能内部源做了裁剪,只保留最近若干个版本。这种情况必须用内部源自己的接口,pip 命令加上--index-url指过去。

问题二:你要的是“历史上发布过什么”,还是“我当前环境能装什么”?这两个答案经常不一样。pip 的所有命令都会应用requires-python过滤、平台兼容性过滤,被撤回(yanked)的版本也会被跳过。你要做依赖考古、要写“某版本是否存在过”的判断,就得绕开 pip,直接读索引。

问题三:给人看还是给程序看?给人看,pip index versions一行命令最省事;给程序看,比如要塞进 CI 做版本存在性校验、要定时拉取新版本做通知,那就老老实实读 JSON 接口,别去解析 pip 的标准错误输出——那个格式没有任何稳定性承诺。

下面这张表是我自己常用的一张对照,先摆出来,后面几节再逐条展开。

路线典型命令数据完整度受当前环境影响适合场景
pip 内置命令pip index versions中,已过滤是日常快速查看
报错信息考古pip install 包名==999中,已过滤是兜底、没装新版 pip
Simple 索引直读curl /simple/<包名>/高,文件级全量否需要全量、需要脚本化
JSON API 直读curl /pypi/<包名>/json高,发布级否要结构化、要版本元数据
本地缓存pip cache list低,仅本机是排查“明明装过”

2. pip 自带的三条路:够用,但各有暗礁

2.1 pip index versions:一条命令的正确打开方式

从 pip 21.2 开始,官方给了pip index这个命令组,查版本用其中这一个子命令最直接:

pip index versions requests

输出大概是这样:

requests (2.31.0) Available versions: 2.31.0, 2.30.0, 2.29.0, 2.28.2, 2.28.1, ... INSTALLED: 2.28.1 LATEST: 2.31.0

第一行是最新版,第二行开始是可用版本列表,后面还会顺带告诉你当前环境装的是哪个版本。这个命令有几个我反复用到的参数,值得记住:

  • --pre:把预发布版本(a/b/rc)和开发版本(dev)也列出来。默认情况下这些是被藏掉的,这就是很多人“查不到 rc 版本”的原因。
  • -i/--index-url:指定源。查内部包时必须加,而且如果源是 http 协议,还得配--trusted-host,否则 pip 会直接拒绝。
  • --extra-index-url:需要同时看主源和附加源时用,比如内部包在私有源、依赖在公网。
  • --no-index --find-links ./wheelhouse:这个组合很实用,可以看本地一个 wheel 仓库目录里都有哪些版本,离线环境救命。

它的局限也很明确。第一,这个命令从诞生起就带着 experimental 标签,老版本 pip 会打印一行警告,虽然实际用下来挺稳,但官方从来没承诺过输出格式不变,千万别拿它做自动化解析。第二,它同样会走 pip 的解析逻辑,requires-python不满足的版本会被筛掉。我遇到过一次,用 Python 3.12 的解释器去查一个早期只声明支持到 3.8 的包,列表明显比网页上短,换成 3.8 环境再查就全了。第三,被 yanked 的版本不会出现在列表里(除非你精确指定)。第四,包名会被规范化——你写Foo_Bar、foo.bar或者FOO-BAR,pip 都会按 PEP 503 转成foo-bar去查,这一点倒是省心,但也容易让人误以为“名字写错了也能查到”。

提示:如果你的 pip 版本低于 21.2,这条命令根本不存在,会报unknown command "index"。先pip install -U pip升级,再考虑后面的备用方案。

2.2 故意装错版本的“报错考古法”

没升级 pip、或者在某些受限环境里,我常用的兜底手法是故意制造一次解析失败,让 pip 在报错信息里把候选版本吐出来:

pip install "requests==999"

你会拿到类似这样的一段:

ERROR: Could not find a version that satisfies the requirement requests==999 (from versions: 2.31.0, 2.30.0, 2.29.0, ...) ERROR: No matching distribution found for requests==999

括号里的from versions:就是当前环境下 pip 认为可用的全部版本。这个技巧的好处是零成本、任何 pip 版本都能用;坏处是列表同样经过了环境过滤,而且当版本特别多的时候,这一行会非常长,终端里翻起来费劲。另外注意版本号别写成空字符串,pip install "requests=="会直接报Invalid requirement,压根不会去查索引。我一般用==999、==0或者==0.0.0.dev0这种一定不存在的值。

同理,pip download也能达到同样效果,而且更“干净”一点,因为它不会真的去碰依赖:

pip download --no-deps -d /tmp/probe "某个包名==999"

这两条命令在内部源上尤其好用——很多企业内部源的 Simple 页面是带鉴权的,浏览器直接访问不方便,但 pip 配置里已经存好了凭证,走 pip 反而是最省事的路径。

2.3 pip cache 与本地 wheel 目录:确认“到底有没有”

还有一种高频场景:同事说“这个包我们明明装过 1.8.2”,你想确认。这时候查远端没意义,得看本地。

pip cache list 某个包名

它会列出缓存里保留的 wheel 文件,文件名里带版本号。注意两点:一是 pip 的 HTTP 缓存默认只保留最近用过的包,老版本很可能已经被清掉了;二是pip cache dir告诉你缓存目录在哪,直接去目录里ls有时候比命令更快。

如果团队有离线部署的习惯,通常会有个 wheelhouse 目录,所有依赖的 wheel 都预先下载好了。查这个目录用:

pip index versions 某个包名 --no-index --find-links ./wheelhouse

这条命令的意义在于:它查的不是公网,而是“我们这个部署包里到底准备了哪些版本”。做交付排查的时候,这比查公网有用得多——公网上有 2.31,不代表你的交付包里带了 2.31。

3. 直捣源头:直接读索引接口

3.1 Simple 索引:最原始也最可靠的数据源

想拿到不受当前环境影响的完整列表,最稳的办法是直接请求 Simple 索引。PyPI 的这个路径永远可用,不需要任何 token:

curl -s https://pypi.org/simple/requests/ | head -50

返回的 HTML 里,每条<a>标签的href都指向一个文件,链接文字就是文件名。要提取版本号,最直接的办法是抓文件名再做拆分:

curl -s https://pypi.org/simple/requests/ \ | grep -oE 'requests-[0-9][^"#<]*\.(whl|tar\.gz)' \ | sed -E 's/^requests-([^-]+).*/\1/' \ | sort -u

这段命令的思路是:先按文件名模式粗筛,再用 sed 抠出版本段,最后去重。对绝大多数包够用,但它有个固有缺陷——如果包名本身带连字符(规范化之后很常见),单纯按第一个-切分会切错。比如包名my-cool-lib的文件名是my_cool_lib-1.2.3-py3-none-any.whl,按第一个-切就没问题;但如果包名里没有下划线化,情况就复杂了。更靠谱的做法是用 JSON 表示:

curl -s -H "Accept: application/vnd.pypi.simple.v1+json" \ https://pypi.org/simple/requests/ \ | jq -r '.files[].filename' | sort -u

这个 JSON 结构里,files数组的每一项都有filename、url、hashes,部分还有requires-python和yanked字段。requires-python这个字段很有价值——它让你能判断某个版本是否兼容你的 Python 版本,而不必真的去装一遍。

Simple 索引还有几个实用细节:同一路径下通常有index.html,但直接请求目录本身也能拿到;企业源如果开了鉴权,可以用curl -u 用户名:密码或者带 token 的 URL;pip download下载时生成的日志里其实也包含 Simple 页面的 URL,排查内部源问题时可以拿来做参照。

3.2 JSON API 与releases字段的三个坑

另一个入口是 JSON API,路径形如https://pypi.org/pypi/<包名>/json。用 jq 提取版本列表非常干净:

curl -s https://pypi.org/pypi/requests/json \ | jq -r '.releases | keys[]' \ | sort -V

或者只保留真正有文件的版本:

curl -s https://pypi.org/pypi/requests/json \ | jq -r '.releases | to_entries[] | select(.value | length > 0) | .key'

这个接口用起来舒服,但有三处容易翻车,我逐个说。

第一,releases里存在空列表。某些版本曾经发布过,后来文件被管理员清理,键还留着,值是[]。如果你的逻辑是“遍历 keys 就算发布过”,就会把这类幽灵版本算进去。做版本存在性判断时,一定要加length > 0这个条件。

第二,yanked 状态藏在文件对象里。被撤回的版本不会从releases里消失,只是每个文件对象上多了yanked: true和yanked_reason。pip 在解析时会跳过这些版本(除非你精确 pin 到它),所以 pip 的列表和 JSON 的列表天然存在差异。我在做依赖审计时吃过一次亏:脚本认为某版本可用,实际装的时候 pip 直接跳过,最后定位到就是被 yanked 了。

第三,响应体积可能很大。像那些迭代了上百个版本、每个版本十几个文件的包,整份 JSON 能有几 MB。如果你只是想要版本号,用它其实有点浪费带宽。我的习惯是:单次查询用 JSON API 图省事,批量查询(比如一次性看几十个包的最新版本)就走 Simple 索引的 JSON 表示,响应小得多。

还有个小细节:info.version字段给的是当前“最新稳定版”,这个值和releases里的最大值不一定一致——因为最大值可能是预发布版,而info.version通常指向最新的正式版。做版本比较时别把这两个概念混在一起。

3.3 内部源怎么套用同一套方法

企业内部的制品库,不管是哪家产品,只要遵循了 Simple 索引规范,上面所有方法都能平移,只需要把域名换掉:

pip index versions 内部包名 --index-url https://内部源地址/simple/ --trusted-host 内部源域名

对 Simple 索引的直接请求也一样:

curl -s -H "Accept: application/vnd.pypi.simple.v1+json" \ https://内部源地址/simple/内部包名/ | jq -r '.files[].filename'

实际会遇到的差异主要有三处。一是路径前缀,有些产品把 Simple 索引挂在/repository/pypi/simple/这类多级路径下,不是根路径,第一次配的时候容易漏。二是鉴权方式,有的是 basic auth,有的是 token 放在 URL 里,有的是走pip.conf里的index-url内嵌凭证,需要确认清楚。三是同步策略,内部源通常是从公网源同步过来的,可能只同步正式版、可能只保留最近 N 个版本、也可能有延迟。同事报“我这里没有 2.30.0”,八成不是他敲错了命令,而是内部源还没同步到。

还有一个容易被忽略的点:pip 支持一次配多个索引(index-url加若干extra-index-url),但pip index versions到底查了哪个源,输出里是不会明说的。排查时我一般直接用 curl 请求 Simple 索引,把每个源都打一遍,比对结果,比猜快得多。

4. 拿到列表之后:解析、排序与筛选

4.1 PEP 440 的版本号结构,以及为什么不能简单按字符串排序

拿到版本列表只是第一步,真正麻烦的是排序和比较。Python 包的版本号遵循 PEP 440,它把一个版本拆成六段:

组成示例说明
epoch1!优先级最高的段,几乎没人用
release2.31.0主版本号,最常比较的部分
prerc1/b2/a1预发布,排在对应正式版之前
post.post1发布后补丁,排在正式版之后
dev.dev3开发版,优先级最低
local+cu118本地版本标识,不参与跨包比较

关键点在于:1.0rc1 < 1.0 < 1.0.post1,而1.0.dev1 < 1.0a1。很多人随手写sort -V或者sorted(list),结果2.10排在2.9前面这种问题还算好,遇到 rc 和 post 就彻底乱套了。sort -V是按字符和数字块比对的,它不认识 PEP 440 的语义,1.0rc1和1.0谁大谁小它给不出正确答案。

所以只要有比较需求,就老老实实用packaging库的Version对象。这个库本来就是 pip 的依赖,环境里基本都有:

from packaging.version import Version versions = ["2.9.0", "2.10.0", "2.10.0rc1", "2.10.0.post1"] print(sorted(versions, key=Version)) # ['2.9.0', '2.10.0rc1', '2.10.0', '2.10.0.post1']

这是我在所有涉及版本比较的脚本里都会用的一行。自己写字符串比较逻辑,早晚会翻车,而且翻车的时候往往是在生产环境的依赖解析上。

4.2 按需过滤:预发布、撤回版本、平台标签

完整的版本列表通常比你想用的长得多。几个常用的过滤维度:

预发布版本。Version对象有is_prerelease属性,True表示是 a/b/rc 这类。做“最新稳定版”判断时,直接过滤掉它们。同理还有is_devrelease和is_postrelease。

被撤回的版本。这个信息只在索引里,Version对象不知道。要判断只能回到数据源:Simple 的 JSON 表示里每个文件有yanked字段,JSON API 里在releases[版本][i].yanked。如果某个版本下所有文件都被 yanked,这个版本实际上是“不可安装但存在过”的状态。

平台与 Python 版本兼容性。看 Simple JSON 里的requires-python字段,可以快速筛掉不兼容的版本,省得装上去再报错。如果关心 wheel 的平台标签,可以从文件名里解析,packaging.utils.parse_wheel_filename能把文件名拆成(包名, 版本, build号, 标签集合),比自己在文件名上做正则可靠得多。

实际工作中我通常做三层过滤:先去掉 yanked,再去掉预发布,最后按 Python 版本要求筛一遍。剩下的才是我真正可以选择的候选集。

4.3 一个可以直接抄的查询脚本

把前面这些拼起来,就是我自己常用的一个小工具。它走 Simple 索引的 JSON 表示,不依赖 pip,不受当前解释器版本影响,输出按版本排序,并标注预发布状态。

import json import sys import urllib.request from packaging.version import Version, InvalidVersion from packaging.utils import parse_sdist_filename, parse_wheel_filename ACCEPT = "application/vnd.pypi.simple.v1+json" def fetch_simple(pkg: str, index_url: str, timeout: int = 20) -> list: url = f"{index_url.rstrip('/')}/{pkg}/" req = urllib.request.Request(url, headers={ "Accept": ACCEPT, "User-Agent": "version-probe/1.0", }) with urllib.request.urlopen(req, timeout=timeout) as resp: return json.load(resp)["files"] def extract_versions(files: list) -> dict: found = {} for item in files: name = item["filename"] yanked = bool(item.get("yanked", False)) requires = item.get("requires-python") or "" try: if name.endswith(".whl"): _, ver, _, _ = parse_wheel_filename(name) elif name.endswith((".tar.gz", ".zip")): _, ver = parse_sdist_filename(name) else: continue except Exception: continue record = found.setdefault(str(ver), {"ver": ver, "yanked": True, "requires": set()}) record["yanked"] = record["yanked"] and yanked if requires: record["requires"].add(requires) return found def main(): pkg = sys.argv[1] index_url = sys.argv[2] if len(sys.argv) > 2 else "https://pypi.org/simple" records = extract_versions(fetch_simple(pkg, index_url)) rows = sorted(records.values(), key=lambda r: r["ver"], reverse=True) stable = [r for r in rows if not r["ver"].is_prerelease and not r["ver"].is_devrelease] print(f"包名: {pkg} 源: {index_url}") print(f"总版本数: {len(rows)} 稳定版本数: {len(stable)}") print(f"最新稳定版: {stable[0]['ver'] if stable else '无'}") print("-" * 60) for r in rows: flags = [] if r["ver"].is_prerelease: flags.append("pre") if r["ver"].is_devrelease: flags.append("dev") if r["ver"].is_postrelease: flags.append("post") if r["yanked"]: flags.append("yanked") tag = ("[" + ",".join(flags) + "]") if flags else "" req = ";".join(sorted(r["requires"])) or "-" print(f"{str(r['ver']):<20} requires-python={req:<15} {tag}") if __name__ == "__main__": main()

用法很简单:

python probe.py requests python probe.py 内部包名 https://内部源地址/simple/

脚本里有几处是我特意这么写的。extract_versions里用了parse_wheel_filename和parse_sdist_filename,而不是自己写正则从文件名里抠版本——这两个函数内部处理了包名规范化和版本段识别的边界情况,我试过自己写,遇到带连字符的包名就崩。yanked的聚合用了and,意思是“只有当这个版本下所有文件都被撤回,才算整个版本被撤回”,这更符合实际语义。requires用集合收集,因为同一个版本的不同文件可能声明不同的 Python 版本要求,全列出来比只留一个更诚实。输出里把预发布、dev、post、yanked 都用标签标出来,一眼就能看出哪些能直接用。

脚本没有处理的是鉴权。如果你的内部源需要认证,最省事的办法不是改脚本,而是把凭证放到~/.netrc里,urllib会自动读取,这样脚本本身保持干净,凭证也不会硬编码在代码里。

5. 常见问题与排查实录

5.1 一张速查表,覆盖八成现场问题

下面这张表是我这些年被问到最多的几类问题,按“现象—原因—处理”整理。遇到症状先对号入座,比从零排查快得多。

现象最可能的原因处理方式
命令不存在,报unknown command "index"pip 版本低于 21.2升级 pip,或改用报错考古法
列表里没有 rc / dev 版本默认过滤预发布加--pre参数
网页上看到了某个版本,pip 里没有该版本被 yanked,或requires-python不满足精确 pin 该版本试装,或换解释器版本重查
内部源查不到某个版本同步延迟、同步策略裁剪直接 curl 内部源的 Simple 路径比对
直接 curl 报 401/403内部源开了鉴权用curl -u或配置 netrc
版本列表顺序混乱用了字符串排序改用packaging.version.Version
脚本判定某版本存在,装的时候却失败releases里有空列表过滤length > 0
文件名解析出的版本对不上包名自带连字符,切分错位用parse_wheel_filename等官方函数
查询超时索引响应体过大,或网络抖动加超时重试,改用 Simple JSON 减小体积

5.2 几个反直觉的坑,值得单独说

坑一:pip search早就不能用了。不少人第一反应是搜一下,结果报XMLRPC request failed。这个搜索接口在很多年前就被关掉了,别在这上面浪费时间,直接用pip index versions。

坑二:包名不规范也能查到,但会让你误判。Foo_Bar、foo.bar、foo-bar在索引层面是同一个包,pip 会自动规范化。这在查的时候是好事,但在做自动化时要注意:你从 Simple 索引里解析出来的文件名,包名部分通常是下划线化的,和你输入的名字不一定是同一个字符串。脚本里做键值匹配时,统一先规范化再比。

坑三:同一个版本号,不同源上的内容可能不一样。公共源和内部源理论上应该一致,但内部源在同步时可能只拉了 wheel 没拉源码包,或者反过来。我在排查一次“本地能装、线上装不上”的问题时,最后定位到就是内部源上那个版本的 wheel 缺失,只有源码包,而线上环境缺少编译依赖。查版本号只是第一步,确认“这个版本下有哪些文件”往往才是关键。

坑四:预发布版本的判定不如想象中简单。2.0.0b1是预发布,2.0.0.dev1是开发版,2.0.0.post1是补丁版,这三个属性是独立的,不要混用is_prerelease去判断 dev 版本。另外还有2.0.0rc1.dev2这种组合形态,两个标记都成立。

坑五:不要解析 pip 的报错信息做生产逻辑。报错文本、措辞、顺序都没有稳定性承诺,pip 换个小版本就可能变。要做正式的版本校验,走索引接口。我用报错考古法只在两种场合:临时手动查,以及环境受限又没有别的办法时。

注意:如果你的脚本要在 CI 里跑,务必给索引请求加上超时和重试。公共源的可用性整体不错,但偶发的慢响应足以让一个没设超时的任务卡满整个流水线时长,这种事故我见过不止一次。

最后分享一点个人体会。这个需求看起来是个一句话就能回答的小问题,但它背后牵扯的东西比想象的多:索引协议的两种表示、版本号的语义化规范、pip 的过滤逻辑、内部源的同步策略。我最初也是被“为什么网页上有、pip 里没有”这种问题逼着去看 Simple 索引的原始响应,看完之后很多疑问一次性消掉了。现在我的习惯是,凡是要把版本信息写进脚本,一律走索引接口,pip 的命令只留给人眼看。这个分工定下来之后,依赖相关的排查时间至少省了一半。

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

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

立即咨询