本文介绍在无法回溯 conda 环境修订版本时,如何通过 Python 标准工具获取 pip 安装包的元信息(如安装路径、时间戳),并指出 pip.get_installed_distributions() 已弃用,推荐使用 pkg_resources.working_set 替代。
本文介绍在无法回溯 conda 环境修订版本时,如何通过 python 标准工具获取 pip 安装包的元信息(如安装路径、时间戳),并指出 `pip.get_installed_distributions()` 已弃用,推荐使用 `pkg_resources.working_set` 替代。
在 Python 包管理实践中,常需追溯某个包的安装时间或环境变更历史,尤其当依赖冲突导致环境异常时——例如你发现 conda 环境中大量包版本被意外升级,而 conda list --revisions 仅显示初始状态(rev 0),未记录后续 pip 安装/卸载操作。这是因为 conda 不追踪 pip 安装行为:所有通过 pip install 或 pip uninstall 引入的包均不会生成 conda revision 记录,其历史需借助 Python 生态自身机制挖掘。
✅ 正确获取已安装包列表及安装时间
pip.get_installed_distributions() 在 pip ≥10.0 中已被彻底移除(引发你遇到的 AttributeError),官方明确不支持调用其内部 API。替代方案是使用 setuptools 提供的稳定接口 pkg_resources.working_set ——它不仅兼容性强(Python 3.6+ 且默认随 pip 安装),还能访问每个分发包(Distribution)的完整元数据。
以下为可靠、跨平台的实现代码:
import pkg_resources
import os
import time
# 获取当前环境中所有已安装的包(包括 conda 和 pip 安装的)
distributions = list(pkg_resources.working_set)
for dist in distributions:
# 尝试获取包安装目录的创建时间(近似安装时间)
try:
location = dist.location
# 注意:location 是包所在目录(如 site-packages/mylib),非 .egg-info 路径
# 更准确的做法是查找 *.dist-info 或 *.egg-info 目录的时间戳
info_dir = None
for suffix in [".dist-info", ".egg-info"]:
candidate = os.path.join(location, dist.project_name + suffix)
if os.path.isdir(candidate):
info_dir = candidate
break
# 兼容大小写不敏感匹配(如 PyYAML-6.0.1.dist-info)
for entry in os.listdir(location):
if (entry.lower().startswith(dist.project_name.lower().replace("-", "_").replace(".", "_")) and
(suffix.lower() in entry.lower())):
info_dir = os.path.join(location, entry)
break
if info_dir and os.path.exists(info_dir):
install_time = os.path.getctime(info_dir)
print(f"{dist.project_name}=={dist.version} | {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(install_time))} | {info_dir}")
else:
print(f"{dist.project_name}=={dist.version} | (no .dist-info/.egg-info found) | {location}")
except (OSError, AttributeError) as e:
print(f"{dist.project_name}=={dist.version} | (error: {e})")? 说明:
- .dist-info(PEP 566)或 .egg-info 目录由安装工具(pip/setuptools)在安装时生成,其创建时间可作为安装时间的合理代理;
- 仅读取 dist.location 的创建时间可能不准(例如虚拟环境整体复制后时间戳不变),故优先定位元数据目录;
- 此方法对 conda install 和 pip install 的包均有效,但无法区分安装方式(无日志级溯源能力)。
⚠️ 重要限制与注意事项
- 无卸载历史:Python 运行时环境不保留“卸载”事件记录。一旦包被 pip uninstall,其元数据即从 working_set 中移除,无法回溯;
- 时间精度局限:文件系统创建时间(ctime)受 OS 影响(如 macOS HFS+ 与 Linux ext4 行为不同),且部分容器/网络文件系统可能不更新 ctime;
- 虚拟环境隔离性:确保在目标 conda 环境中运行该脚本(conda activate myenv && python check_pip_history.py),否则将读取 base 环境;
- 未来演进提示:pkg_resources 已标记为 legacy,PEP 637 推荐迁移到 importlib.metadata(Python 3.8+)。等效现代写法如下:
from importlib import metadata
import time
import os
try:
# Python 3.8+
for dist in metadata.distributions():
try:
# 获取 dist-info 路径
dist_info = dist._path if hasattr(dist, '_path') else dist._path.parent
install_time = os.path.getctime(dist_info)
print(f"{dist.metadata['Name']}=={dist.version} | {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(install_time))}")
except (OSError, AttributeError):
print(f"{dist.metadata['Name']}=={dist.version} | (no accessible timestamp)")
except ImportError:
print("importlib.metadata not available; falling back to pkg_resources...")✅ 总结建议
| 场景 | 推荐方案 |
|---|---|
| 快速查看当前所有包及其大致安装时间 | 使用 pkg_resources.working_set + .dist-info 时间戳 |
| 新项目/Python ≥3.8 环境 | 优先采用 importlib.metadata.distributions() |
| 需要完整操作审计(安装/升级/卸载) | 必须启用外部日志:pip install --log /path/to/pip.log 或配置 ~/.pip/pip.conf 设置 log = /path/to/pip.log;conda 用户应统一使用 conda install 并定期备份 conda env export > environment.yml |
最终,请牢记:包管理器的历史可追溯性取决于主动日志策略,而非被动环境扫描。在关键开发或生产环境中,应建立标准化流程——例如每次变更后执行 pip freeze > requirements.txt 并提交至版本控制,这才是真正可靠的“历史”。










