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:
@ -25,105 +25,203 @@ import sys
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def _find_and_set_gdal_env():
|
||||
"""动态推断并强设 PROJ_LIB 和 GDAL_DATA 环境变量
|
||||
"""动态推断并强设 PROJ_LIB 和 GDAL_DATA 环境变量。
|
||||
|
||||
按优先级搜索 proj.db 和 GDAL data 目录:
|
||||
1. CONDA_PREFIX 环境变量(当前激活的 Conda 环境)
|
||||
2. sys.prefix(Python 安装前缀)
|
||||
3. CONDA_ROOT / CONDA_BASE(base 环境)
|
||||
4. 常见 Conda 安装目录
|
||||
5. 硬编码回退路径(WQ_GUI 环境)
|
||||
解决"C:\\ITRES\\ATK\\app\\GDAL\\projlib"等旧系统 PROJ 安装的干扰:
|
||||
强制指向与当前 GDAL 版本匹配的 proj.db(通常在同 Conda 环境的
|
||||
Library/share/proj 下),而非系统路径中的过时版本。
|
||||
|
||||
搜索策略(按优先级):
|
||||
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"""
|
||||
proj_db = os.path.join(path, "proj.db")
|
||||
if os.path.isfile(proj_db):
|
||||
return path
|
||||
# ── 辅助函数 ──
|
||||
def _check_proj_db_version(db_path):
|
||||
"""读取 proj.db 的 SQLite 头,返回主版本号。失败返回 -1。"""
|
||||
try:
|
||||
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
|
||||
|
||||
def _check_gdal(path):
|
||||
"""检查路径下是否含 GDAL 数据文件(以 gdalvrt.xsd 或 pcs.csv 为准)"""
|
||||
for marker in ("gdalvrt.xsd", "pcs.csv", "gcs.csv"):
|
||||
if os.path.isfile(os.path.join(path, marker)):
|
||||
return path
|
||||
def _find_gdal_in_prefix(prefix):
|
||||
"""在指定前缀下查找 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"):
|
||||
if os.path.isfile(os.path.join(path, marker)):
|
||||
return path
|
||||
return None
|
||||
|
||||
# --- 构建搜索前缀列表 ---
|
||||
# ── 构建搜索前缀 ──
|
||||
prefixes = []
|
||||
|
||||
# 1) CONDA_PREFIX(当前激活环境,最优先)
|
||||
conda_prefix = os.environ.get("CONDA_PREFIX", "")
|
||||
if conda_prefix:
|
||||
prefixes.append(conda_prefix)
|
||||
# 1) CONDA_PREFIX + sys.prefix(最优先)
|
||||
for src in (os.environ.get("CONDA_PREFIX", ""), sys.prefix):
|
||||
if src and src not in prefixes:
|
||||
prefixes.append(src)
|
||||
|
||||
# 2) sys.prefix(Python 运行环境)
|
||||
if sys.prefix and sys.prefix not in prefixes:
|
||||
prefixes.append(sys.prefix)
|
||||
# 2) 从 Python 可执行文件路径回推(最可靠:python.exe 就在 {prefix}/ 下)
|
||||
try:
|
||||
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"):
|
||||
val = os.environ.get(key, "")
|
||||
if val and val not in prefixes:
|
||||
prefixes.append(val)
|
||||
|
||||
# 4) 从 PATH 推断 conda 安装根目录
|
||||
for path_dir in os.environ.get("PATH", "").split(os.pathsep):
|
||||
lower = path_dir.lower()
|
||||
if "anaconda" in lower or "miniconda" in lower or "anconda" in lower:
|
||||
# 提取 conda 根目录: .../anconda/ 或 .../anconda/envs/xxx
|
||||
if os.path.basename(path_dir).lower() == "condabin":
|
||||
root = os.path.dirname(path_dir)
|
||||
elif lower.endswith("scripts") or lower.endswith("bin"):
|
||||
root = os.path.dirname(path_dir)
|
||||
# 4) 从 PATH 推断
|
||||
for d in os.environ.get("PATH", "").split(os.pathsep):
|
||||
if not d or d in prefixes:
|
||||
continue
|
||||
lower = d.lower()
|
||||
if any(kw in lower for kw in ("anaconda", "miniconda", "anconda")):
|
||||
# 找到 conda 根: 去掉 /Scripts 或 /condabin
|
||||
if os.path.basename(d).lower() in ("scripts", "condabin", "bin"):
|
||||
root = os.path.dirname(d)
|
||||
else:
|
||||
root = path_dir
|
||||
root = d
|
||||
if root and root not in prefixes:
|
||||
prefixes.append(root)
|
||||
|
||||
# 5) 硬编码回退(用户已知的 WQ_GUI 路径 + 常见目录)
|
||||
hardcoded = [
|
||||
# 5) 已知硬编码路径
|
||||
for p in [
|
||||
r"D:\111\changyongruanjian\anconda\envs\WQ_GUI",
|
||||
r"D:\111\changyongruanjian\anconda",
|
||||
r"C:\ProgramData\anaconda3",
|
||||
r"C:\ProgramData\miniconda3",
|
||||
]
|
||||
for p in hardcoded:
|
||||
]:
|
||||
if os.path.isdir(p) and p not in prefixes:
|
||||
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
|
||||
gdal_data = None
|
||||
|
||||
for prefix in prefixes:
|
||||
if not proj_lib:
|
||||
for sub in ("Library/share/proj", "share/proj", "Library\\share\\proj", "share\\proj"):
|
||||
path = os.path.join(prefix, sub)
|
||||
proj_lib = _check_proj(path)
|
||||
if proj_lib:
|
||||
# 先尝试标准子目录(快速路径)
|
||||
for sub in ("Library/share/proj", "share/proj",
|
||||
"Library\\share\\proj", "share\\proj"):
|
||||
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
|
||||
# 标准路径未找到 → 递归搜索(仅对 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:
|
||||
for sub in ("Library/share/gdal", "share/gdal", "Library\\share\\gdal", "share\\gdal"):
|
||||
path = os.path.join(prefix, sub)
|
||||
gdal_data = _check_gdal(path)
|
||||
if gdal_data:
|
||||
break
|
||||
gdal_data = _find_gdal_in_prefix(prefix)
|
||||
if proj_lib and gdal_data:
|
||||
break
|
||||
|
||||
# ── 应用环境变量 ──
|
||||
if proj_lib:
|
||||
os.environ["PROJ_LIB"] = proj_lib
|
||||
print(f"[ENV] PROJ_LIB → {proj_lib}")
|
||||
else:
|
||||
print("[ENV] ⚠ 未找到 proj.db,PROJ_LIB 保持系统默认,可能影响坐标转换")
|
||||
print(f"[ENV] (已搜索 {len(prefixes)} 个前缀: {prefixes[:3]}...)")
|
||||
print("[ENV] ⚠ 未找到兼容的 proj.db (需要 PROJ v6+),PROJ_LIB 保持系统默认")
|
||||
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:
|
||||
os.environ["GDAL_DATA"] = gdal_data
|
||||
print(f"[ENV] GDAL_DATA → {gdal_data}")
|
||||
else:
|
||||
print("[ENV] ⚠ 未找到 GDAL data 目录,GDAL_DATA 保持系统默认")
|
||||
print(f"[ENV] (已搜索 {len(prefixes)} 个前缀: {prefixes[:3]}...)")
|
||||
print(f"[ENV] (已搜索 {len(prefixes)} 个前缀)")
|
||||
|
||||
_find_and_set_gdal_env()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user