fix: 彻底解决 PROJ 版本冲突 — 深度搜索 conda env + pip wheel 中的 proj.db

问题: step11 所有内容报错:
  'C:\ITRES\ATK\app\GDAL\projlib\proj.db contains
   DATABASE.LAYOUT.VERSION.MINOR = 2 whereas a number >= 6 is expected'
原因: 系统有旧版 GDAL (C:\ITRES\ATK\app\GDAL), 其 proj.db 为 v2,
      GDAL 内部搜索先找到旧版 → PROJ 坐标转换全部失败

增强的搜索策略 (由简到深):
  1. Python 可执行文件位置回推 prefix (最可靠)
  2. sys.path 中 site-packages 回推所有 conda/env 前缀
  3. CONDA_PREFIX + sys.prefix + CONDA_ROOT
  4. PATH 中推断 conda 安装路径
  5. 硬编码已知路径
  6. 扫描 conda 根下的 envs/*/ 所有环境
  7. pip wheel 路径: {site-packages}/osgeo/data/proj/

新增 proj.db 版本检测:
  - _check_proj_db_version(): 读取 SQLite 头,
    通过文件大小判定 v6+ (>3MB) 或 v2 (<3MB)
  - 仅选择 v6+ 的 proj.db 设置 PROJ_LIB

新增递归搜索:
  - _find_proj_in_prefix(): 在 prefix 下深度搜索 Library/share/proj/
    限制深度 6 层避免全盘扫描

找不到时打印明确的修复建议:
  '请手动设置: set PROJ_LIB=<conda环境>\Library\share\proj'
This commit is contained in:
duxin
2026-07-07 16:23:38 +08:00
parent c6e42c3d2f
commit da773b139f

View File

@ -25,105 +25,203 @@ import sys
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
def _find_and_set_gdal_env(): def _find_and_set_gdal_env():
"""动态推断并强设 PROJ_LIB 和 GDAL_DATA 环境变量 """动态推断并强设 PROJ_LIB 和 GDAL_DATA 环境变量。
按优先级搜索 proj.db 和 GDAL data 目录: 解决"C:\\ITRES\\ATK\\app\\GDAL\\projlib"等旧系统 PROJ 安装的干扰:
1. CONDA_PREFIX 环境变量(当前激活的 Conda 环境) 强制指向与当前 GDAL 版本匹配的 proj.db(通常在同 Conda 环境的
2. sys.prefix(Python 安装前缀) Library/share/proj 下),而非系统路径中的过时版本。
3. CONDA_ROOT / CONDA_BASE(base 环境)
4. 常见 Conda 安装目录 搜索策略(按优先级):
5. 硬编码回退路径(WQ_GUI 环境) 1. CONDA_PREFIX + sys.prefix 的 Library/share/ 子目录
2. 搜索 sys.path 找到 osgeo 模块所在位置,回推 Library 路径
3. CONDA_ROOT / base 环境的 Library 路径
4. PATH 中推断的 conda 安装路径
5. 已知的硬编码回退路径
""" """
def _check_proj(path): # ── 辅助函数 ──
"""检查路径下是否含有效的 proj.db""" def _check_proj_db_version(db_path):
proj_db = os.path.join(path, "proj.db") """读取 proj.db 的 SQLite 头,返回主版本号。失败返回 -1。"""
if os.path.isfile(proj_db): try:
return path with open(db_path, "rb") as f:
header = f.read(100)
# SQLite 3 文件头: "SQLite format 3\0",之后是页大小等
# DATABASE.LAYOUT.VERSION 在 offset 60 附近的 schema 中,
# 但最简单的是读整个头中 "major" / "minor" 文本
if b"SQLite format 3" not in header:
return -2
# PROJ 在 proj.db 的 metadata 表存储版本,我们直接看文件大小
# 粗略判定:< 2MB → v2-v4, 2~6MB → v5-v6, > 6MB → v7+
size_mb = os.path.getsize(db_path) / (1024 * 1024)
return 6 if size_mb > 3.0 else 2 # v6+ 的 proj.db 通常 > 3MB
except Exception:
return -1
def _find_proj_in_prefix(prefix):
"""在指定前缀下递归搜索 Library/share/proj/proj.db"""
candidates = []
for root, dirs, files in os.walk(prefix):
# 限制深度避免全盘扫描
depth = root.replace(prefix, "").count(os.sep)
if depth > 6:
dirs.clear()
continue
if "proj.db" in files:
db_path = os.path.join(root, "proj.db")
ver = _check_proj_db_version(db_path)
if ver >= 6:
candidates.append((ver, root))
# 跳过明显的非目标目录
dirs[:] = [d for d in dirs if d not in (".git", "__pycache__", "node_modules")]
if candidates:
candidates.sort(key=lambda x: -x[0]) # 最新版本优先
return candidates[0][1]
return None return None
def _check_gdal(path): def _find_gdal_in_prefix(prefix):
"""检查路径下是否含 GDAL 数据文件(以 gdalvrt.xsd 或 pcs.csv 为准)""" """在指定前缀下查找 Library/share/gdal"""
for sub in ("Library/share/gdal", "share/gdal",
"Library\\share\\gdal", "share\\gdal"):
path = os.path.join(prefix, sub)
if os.path.isdir(path):
for marker in ("gdalvrt.xsd", "pcs.csv", "gcs.csv"): for marker in ("gdalvrt.xsd", "pcs.csv", "gcs.csv"):
if os.path.isfile(os.path.join(path, marker)): if os.path.isfile(os.path.join(path, marker)):
return path return path
return None return None
# --- 构建搜索前缀列表 --- # ── 构建搜索前缀 ──
prefixes = [] prefixes = []
# 1) CONDA_PREFIX(当前激活环境,最优先) # 1) CONDA_PREFIX + sys.prefix(最优先)
conda_prefix = os.environ.get("CONDA_PREFIX", "") for src in (os.environ.get("CONDA_PREFIX", ""), sys.prefix):
if conda_prefix: if src and src not in prefixes:
prefixes.append(conda_prefix) prefixes.append(src)
# 2) sys.prefix(Python 运行环境) # 2) 从 Python 可执行文件路径回推(最可靠:python.exe 就在 {prefix}/ 下)
if sys.prefix and sys.prefix not in prefixes: try:
prefixes.append(sys.prefix) py_exe = sys.executable
py_prefix = os.path.dirname(py_exe) # python.exe 所在目录
if os.path.basename(py_prefix).lower() == "scripts":
# Windows venv: {prefix}/Scripts/python.exe → 前缀是父目录
exe_prefix = os.path.dirname(py_prefix)
else:
exe_prefix = py_prefix
if exe_prefix and exe_prefix not in prefixes:
prefixes.insert(0, exe_prefix) # 插到最前面,最高优先级
except Exception:
pass
# 3) CONDA_ROOT / 其他 conda 线索 # 3) 从 sys.path 中 site-packages 位置回推所有可能的 conda/env 前缀
try:
for sp in sys.path:
if not sp or sp in prefixes:
continue
norm = os.path.normpath(sp)
parts = norm.split(os.sep)
# 检查 sp 是否在 site-packages 下 → 回推到 {prefix}
for marker in ("site-packages", "Lib"):
if marker in parts:
idx = parts.index(marker)
possible = os.sep.join(parts[:idx])
if os.path.isdir(possible) and possible not in prefixes:
prefixes.append(possible)
break
except Exception:
pass
# 3) CONDA_ROOT / base 环境
for key in ("CONDA_ROOT", "MAMBA_ROOT_PREFIX"): for key in ("CONDA_ROOT", "MAMBA_ROOT_PREFIX"):
val = os.environ.get(key, "") val = os.environ.get(key, "")
if val and val not in prefixes: if val and val not in prefixes:
prefixes.append(val) prefixes.append(val)
# 4) 从 PATH 推断 conda 安装根目录 # 4) 从 PATH 推断
for path_dir in os.environ.get("PATH", "").split(os.pathsep): for d in os.environ.get("PATH", "").split(os.pathsep):
lower = path_dir.lower() if not d or d in prefixes:
if "anaconda" in lower or "miniconda" in lower or "anconda" in lower: continue
# 提取 conda 根目录: .../anconda/ 或 .../anconda/envs/xxx lower = d.lower()
if os.path.basename(path_dir).lower() == "condabin": if any(kw in lower for kw in ("anaconda", "miniconda", "anconda")):
root = os.path.dirname(path_dir) # 找到 conda 根: 去掉 /Scripts 或 /condabin
elif lower.endswith("scripts") or lower.endswith("bin"): if os.path.basename(d).lower() in ("scripts", "condabin", "bin"):
root = os.path.dirname(path_dir) root = os.path.dirname(d)
else: else:
root = path_dir root = d
if root and root not in prefixes: if root and root not in prefixes:
prefixes.append(root) prefixes.append(root)
# 5) 硬编码回退(用户已知的 WQ_GUI 路径 + 常见目录) # 5) 已知硬编码路径
hardcoded = [ for p in [
r"D:\111\changyongruanjian\anconda\envs\WQ_GUI", r"D:\111\changyongruanjian\anconda\envs\WQ_GUI",
r"D:\111\changyongruanjian\anconda", r"D:\111\changyongruanjian\anconda",
r"C:\ProgramData\anaconda3", r"C:\ProgramData\anaconda3",
r"C:\ProgramData\miniconda3", r"C:\ProgramData\miniconda3",
] ]:
for p in hardcoded:
if os.path.isdir(p) and p not in prefixes: if os.path.isdir(p) and p not in prefixes:
prefixes.append(p) prefixes.append(p)
# --- 对每个前缀尝试 Windows + Linux 子目录 --- # ── 额外:扫描 conda 根目录下的 envs/*/Library/share/proj ──
_extra_prefixes = []
for prefix in list(prefixes):
envs_dir = os.path.join(prefix, "envs")
if os.path.isdir(envs_dir):
try:
for entry in os.listdir(envs_dir):
env_path = os.path.join(envs_dir, entry)
if os.path.isdir(env_path) and env_path not in prefixes:
_extra_prefixes.append(env_path)
except Exception:
pass
prefixes.extend(_extra_prefixes)
# 6) pip wheel GDAL: {site-packages}/osgeo/data/proj/proj.db
# (conda-forge GDAL wheel on Windows 有时把 proj.db 放这里)
try:
import site as _site
for _sp in _site.getsitepackages():
pip_proj = os.path.join(_sp, "osgeo", "data", "proj")
if os.path.isfile(os.path.join(pip_proj, "proj.db")):
pip_gdal = os.path.join(_sp, "osgeo", "data", "gdal")
if _sp not in prefixes:
prefixes.append(_sp)
except Exception:
pass
proj_lib = None proj_lib = None
gdal_data = None gdal_data = None
for prefix in prefixes: for prefix in prefixes:
if not proj_lib: if not proj_lib:
for sub in ("Library/share/proj", "share/proj", "Library\\share\\proj", "share\\proj"): # 先尝试标准子目录(快速路径)
path = os.path.join(prefix, sub) for sub in ("Library/share/proj", "share/proj",
proj_lib = _check_proj(path) "Library\\share\\proj", "share\\proj"):
if proj_lib: db_path = os.path.join(prefix, sub, "proj.db")
if os.path.isfile(db_path) and _check_proj_db_version(db_path) >= 6:
proj_lib = os.path.join(prefix, sub)
break break
# 标准路径未找到 → 递归搜索(仅对 conda 环境前缀)
if not proj_lib and (
os.path.basename(prefix) not in ("", "share", "bin", "Scripts")
):
proj_lib = _find_proj_in_prefix(prefix)
if not gdal_data: if not gdal_data:
for sub in ("Library/share/gdal", "share/gdal", "Library\\share\\gdal", "share\\gdal"): gdal_data = _find_gdal_in_prefix(prefix)
path = os.path.join(prefix, sub)
gdal_data = _check_gdal(path)
if gdal_data:
break
if proj_lib and gdal_data: if proj_lib and gdal_data:
break break
# ── 应用环境变量 ──
if proj_lib: if proj_lib:
os.environ["PROJ_LIB"] = proj_lib os.environ["PROJ_LIB"] = proj_lib
print(f"[ENV] PROJ_LIB → {proj_lib}") print(f"[ENV] PROJ_LIB → {proj_lib}")
else: else:
print("[ENV] ⚠ 未找到 proj.db,PROJ_LIB 保持系统默认,可能影响坐标转换") print("[ENV] ⚠ 未找到兼容的 proj.db (需要 PROJ v6+),PROJ_LIB 保持系统默认")
print(f"[ENV] (已搜索 {len(prefixes)} 个前缀: {prefixes[:3]}...)") print(f"[ENV] (已搜索 {len(prefixes)} 个前缀: {[os.path.basename(p) for p in prefixes[:5]]}...)")
print("[ENV] 如果 step11 地图渲染报 PROJ 版本错误,")
print("[ENV] 请手动设置: set PROJ_LIB=<conda环境>\\Library\\share\\proj")
if gdal_data: if gdal_data:
os.environ["GDAL_DATA"] = gdal_data os.environ["GDAL_DATA"] = gdal_data
print(f"[ENV] GDAL_DATA → {gdal_data}") print(f"[ENV] GDAL_DATA → {gdal_data}")
else: else:
print("[ENV] ⚠ 未找到 GDAL data 目录,GDAL_DATA 保持系统默认") print("[ENV] ⚠ 未找到 GDAL data 目录,GDAL_DATA 保持系统默认")
print(f"[ENV] (已搜索 {len(prefixes)} 个前缀: {prefixes[:3]}...)") print(f"[ENV] (已搜索 {len(prefixes)} 个前缀)")
_find_and_set_gdal_env() _find_and_set_gdal_env()