QGIS提升速度

This commit is contained in:
wzy-warehouse
2026-06-21 13:29:19 +08:00
parent fe3ccd005d
commit 5169ed2f33
28 changed files with 444 additions and 394 deletions
+79 -58
View File
@@ -14,7 +14,6 @@ from .template_modifier import TemplateModifier
from .layer_filter import LayerFilter
from .map_exporter import MapExporter
from app.utils.map_zoom import MapZoom
from app.config.paths import get_logger
logger = get_logger("qgis.service")
@@ -29,43 +28,58 @@ class MapService:
def generate(self, model: dict) -> str:
"""
执行完整的地图生成流程。
Args:
model: 包含地图参数的字典
Returns:
地图名称
"""
t_start = time.time()
_timing = {} # {step: seconds}
t_total = time.time()
template_path = model["path"]
template_name = os.path.basename(template_path)
project = QgsProject.instance()
is_cache_hit = template_cache.is_loaded(template_path)
# 加载/恢复模板
# ── 步骤 1加载/恢复模板 ──
t0 = time.time()
if is_cache_hit:
logger.info(f"[缓存命中] {os.path.basename(template_path)}")
logger.info(f"[缓存命中] {template_name}")
project, texts, extent = template_cache.restore_template(template_path)
template_cache.reset_project_state(project, texts, extent)
# FlagDontResolveLayers 跳过了数据库连接,手动触发 PostgreSQL 图层解析
self._resolve_postgres_layers(project)
else:
logger.info(f"[首次加载] {os.path.basename(template_path)}")
# 直接读原始模板,不做文件级修改(避免 ZIP 兼容性问题)
project.read(template_path)
logger.info(f"[首次加载] {template_name}")
modifier = TemplateModifier(self.config)
actual_path = modifier.modify(template_path)
project.read(actual_path)
if actual_path != template_path:
try:
os.remove(actual_path)
except OSError:
pass
_timing["1.load"] = time.time() - t0
# 更新图层连接 + GPKG/SRID/表名修正(仅首次加载)
# ── 步骤 2:图层连接修正 ──
t0 = time.time()
if not is_cache_hit:
self._update_db_connections(project)
self._fix_invalid_layers(project)
_timing["2.connections"] = time.time() - t0
# 图层过滤
# ★ 首次加载完成后再保存缓存(确保图层已修正)
if not is_cache_hit:
template_cache.save_current(template_path, model.get("mapLayout", "A3"))
# ── 步骤 3:图层过滤 ──
t0 = time.time()
LayerFilter().apply(project, model)
_timing["3.filter"] = time.time() - t0
# 地图缩放
# ── 步骤 4地图缩放 ──
t0 = time.time()
layout = project.layoutManager().layoutByName(model["mapLayout"])
if layout is None:
available = [l.name() for l in project.layoutManager().layouts()]
raise RuntimeError(
f"模板中未找到布局 '{model['mapLayout']}',可用布局:{available}"
)
map_item = layout.itemById("Map")
zoom = MapZoom(project, layout, map_item)
zoom.execute(model["zoomRule"], {
@@ -73,61 +87,69 @@ class MapService:
"Y": model["centerY"],
"value": model["zoomValue"],
})
_timing["4.zoom"] = time.time() - t0
# 文本更新 + 比例尺 + 导出
# ── 步骤 5文本 + 比例尺 + 导出 ──
t0 = time.time()
exporter = MapExporter(self.config, layout)
exporter.update_texts(model)
exporter.update_scale_bar()
exporter.export(model["outFile"])
_timing["5.export"] = time.time() - t0
elapsed = time.time() - t_start
total = time.time() - t_total
steps = ", ".join(f"{k}={v:.1f}s" for k, v in _timing.items())
logger.info(
f"{'[缓存命中]' if is_cache_hit else '[首次加载]'} "
f"导出完成: {model['name']},耗时 {elapsed:.1f}s"
f"{template_name} {total:.1f}s ({steps})"
)
return model["name"]
def _update_db_connections(self, project: QgsProject) -> None:
"""更新图层连接 + SRID修正 + GPKG静态层替换"""
def _fix_invalid_layers(self, project: QgsProject) -> None:
"""只修复 TemplateModifier 处理后仍无效的图层(TemplateModifier 已修正大部分连接)"""
t0 = time.time()
db_config = self.config["db"]
override = self.config.get("template_override", {})
actual_schema = override.get("actual", {}).get("schema", "qgis")
static_config = self.config.get("static_layers", {})
static_enabled = static_config.get("enabled", False)
gpkg_dir = static_config.get("gpkg_dir", "")
static_layers_map = static_config.get("layers", {})
static_count = 0
fixed = 0
failed = 0
total = 0
for layer in project.mapLayers().values():
provider = layer.providerType()
# GPKG 静态层替换:postgres 表 → 本地 GPKG 文件
if provider == "postgres" and static_enabled:
uri_str = layer.dataProvider().uri().uri()
for name, info in static_layers_map.items():
table_key = info["table"]
schema, table = table_key.split(".", 1)
if f'table="{schema}"."{table}"' in uri_str:
gpkg_path = os.path.join(gpkg_dir, info["file"]).replace("\\", "/")
layer.setDataSource(gpkg_path, name, "ogr")
static_count += 1
logger.debug(f"静态图层 {name} → GPKG")
break
else:
# 没匹配到静态层,继续处理为 postgres
self._fix_postgres_layer(layer, db_config, actual_schema)
if layer.providerType() != "postgres":
continue
if provider == "ogr":
static_count += 1
continue
if provider == "postgres":
total += 1
if layer.isValid():
continue # TemplateModifier 已修正,跳过
try:
self._fix_postgres_layer(layer, db_config, actual_schema)
if layer.isValid():
fixed += 1
else:
failed += 1
except Exception as e:
logger.error(f" 修复图层 {layer.name()} 失败: {e}")
failed += 1
if static_count:
logger.info(f"静态底图已本地化: {static_count} 个图层")
elapsed = time.time() - t0
if total:
logger.info(
f" 图层修正: 总计{total}个PG层, 跳过{total - fixed - failed}(已有效), "
f"修复{fixed}, 仍失败{failed}, 耗时{elapsed:.1f}s"
)
@staticmethod
def _resolve_postgres_layers(project: QgsProject) -> None:
"""FlagDontResolveLayers 跳过了数据库连接,重新 setDataSource 触发"""
for layer in project.mapLayers().values():
if layer.providerType() != "postgres":
continue
try:
source = layer.source()
if source:
layer.setDataSource(source, layer.name(), "postgres")
except Exception:
pass
@staticmethod
def _fix_postgres_layer(layer, db_config, actual_schema):
@@ -168,9 +190,8 @@ class MapService:
layer.setDataSource(uri.uri(), layer.name(), "postgres")
if layer.isValid():
fc = layer.featureCount()
logger.info(f"图层 {layer.name()} 连接更新成功 ({fc} features)")
logger.debug(f" 图层 {layer.name()} 连接更新成功")
else:
logger.error(f"图层 {layer.name()} 更新后仍无效")
logger.error(f" 图层 {layer.name()} 更新后仍无效")
except Exception as e:
logger.error(f"更新图层 {layer.name()} 连接失败: {e}")
logger.error(f" 更新图层 {layer.name()} 连接失败: {e}")
+6
View File
@@ -138,6 +138,12 @@ def main():
# 初始化 QgsApplication(只做一次)
qgs_app = _init_qgis()
# 从磁盘恢复已缓存的模板(跨进程加速)
from app.services.qgis.map_service import template_cache
cached_count = template_cache.load_persistent_cache()
if cached_count > 0:
print(f"[qgis_runner] 磁盘缓存命中: {cached_count} 个模板", file=sys.stderr)
try:
from app.services.qgis.map_service import MapService
+115 -41
View File
@@ -1,13 +1,15 @@
"""
模板缓存引擎。解决 QgsProject 单例问题
模板缓存引擎(支持持久化)
流程:
1. 首次请求:project.read() 加载模板(慢,仅一次)
2. 加载后 project.write() 保存到临时文件
3. 后续同模板请求:从临时文件恢复(快,连接复用
4. 手动恢复文本/过滤/缩放(毫秒级)
1. 首次加载:TemplateModifier 修改模板 → project.read() → 保存到磁盘缓存
2. 同进程内缓存命中:从内存恢复(~1s
3. 跨进程/重启:从磁盘缓存恢复(首次 ~5s,后续 ~1s
"""
import hashlib
import json
import os
import shutil
import time
import tempfile
@@ -17,36 +19,37 @@ from app.config.paths import get_logger
logger = get_logger("qgis.cache")
# 持久化缓存目录(项目根目录下)
_CACHE_DIR = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),
"app", "data", "cache", "qgis_templates",
)
_INDEX_FILE = os.path.join(_CACHE_DIR, "index.json")
class TemplateCache:
def __init__(self):
self._cache: dict[str, dict] = {}
# ── 公开接口 ──
def is_loaded(self, template_path: str) -> bool:
return template_path in self._cache
def load_template(self, template_path: str, layout_name: str = "A4") -> None:
"""首次加载模板"""
start = time.time()
def save_current(self, template_path: str, layout_name: str = "A4") -> None:
"""将当前已加载的项目保存到内存缓存 + 磁盘缓存"""
project = QgsProject.instance()
logger.info(f"首次加载: {os.path.basename(template_path)}")
project.read(template_path)
logger.info(f"project.read() 耗时: {time.time() - start:.1f}s")
# 保存到临时文件
tmp_file = tempfile.NamedTemporaryFile(
suffix=".qgz", delete=False,
dir=tempfile.gettempdir(),
)
tmp_path = tmp_file.name
tmp_file.close()
# ── 保存项目到持久化缓存目录 ──
os.makedirs(_CACHE_DIR, exist_ok=True)
key = self._path_key(template_path)
cache_file = os.path.join(_CACHE_DIR, f"{key}.qgz")
t_save = time.time()
project.write(tmp_path)
logger.info(f"项目保存耗时: {time.time() - t_save:.1f}s")
project.write(cache_file)
logger.info(f" 缓存写入磁盘: {os.path.basename(cache_file)} ({time.time() - t_save:.1f}s)")
# 记录初始状态
# ── 记录状态 ──
texts = {}
extent = None
layout = project.layoutManager().layoutByName(layout_name)
@@ -59,38 +62,41 @@ class TemplateCache:
if map_item:
extent = QgsRectangle(map_item.extent())
self._cache[template_path] = {
"file": tmp_path,
entry = {
"file": cache_file,
"texts": texts,
"extent": extent,
"layout": layout_name,
}
logger.info(f"模板加载完成,总耗时: {time.time() - start:.1f}s")
self._cache[template_path] = entry
# ── 更新索引文件 ──
self._update_index(template_path, entry)
logger.info(f" 模板已缓存: {os.path.basename(template_path)} ({len(self._cache)} 个)")
def restore_template(self, template_path: str) -> tuple:
"""从缓存恢复模板"""
"""从缓存恢复模板(跳过图层解析以加速)"""
cached = self._cache.get(template_path)
if not cached:
raise RuntimeError(f"模板未缓存: {template_path}")
start = time.time()
project = QgsProject.instance()
t0 = time.time()
logger.info(f"恢复模板: {os.path.basename(template_path)}")
project.read(cached["file"])
logger.info(f"project.read() 耗时: {time.time() - start:.1f}s")
# FlagDontResolveLayers: 跳过数据库连接验证,缓存文件已含正确连接
flags = QgsProject.ReadFlags()
flags |= QgsProject.FlagDontResolveLayers
project.read(cached["file"], flags)
logger.debug(f" 恢复耗时: {time.time() - t0:.1f}s")
return project, cached["texts"], cached["extent"]
def reset_project_state(self, project: QgsProject, texts: dict, extent) -> None:
"""重置项目到干净状态"""
start = time.time()
for layer in project.mapLayers().values():
if layer.subsetString():
layer.setSubsetString("")
# 获取 layout 名称(从缓存中)
layout_name = "A4"
for cached in self._cache.values():
layout_name = cached.get("layout", "A4")
@@ -107,14 +113,82 @@ class TemplateCache:
if map_item:
map_item.zoomToExtent(extent)
logger.info(f"状态重置耗时: {time.time() - start:.3f}s")
def load_persistent_cache(self) -> int:
"""从磁盘恢复所有缓存到内存(qgis_runner 启动时调用)"""
if not os.path.isfile(_INDEX_FILE):
logger.info(" 磁盘缓存为空,首次启动需要加载模板")
return 0
try:
with open(_INDEX_FILE, "r", encoding="utf-8") as f:
index = json.load(f)
except (json.JSONDecodeError, OSError) as e:
logger.warning(f" 缓存索引损坏: {e}")
return 0
count = 0
for template_path, entry in index.get("templates", {}).items():
cache_file = entry.get("file", "")
if not cache_file or not os.path.isfile(cache_file):
logger.debug(f" 跳过无效缓存: {os.path.basename(template_path)}")
continue
texts = entry.get("texts", {})
extent = entry.get("extent")
# 反序列化 extent
if extent and isinstance(extent, dict):
extent = QgsRectangle(
extent.get("xmin", 0), extent.get("ymin", 0),
extent.get("xmax", 0), extent.get("ymax", 0),
)
else:
extent = None
self._cache[template_path] = {
"file": cache_file,
"texts": texts,
"extent": extent,
"layout": entry.get("layout", "A4"),
}
count += 1
logger.info(f" 磁盘缓存加载: {count} 个模板")
return count
def cleanup(self) -> None:
"""清理所有临时文件"""
for cached in self._cache.values():
try:
os.remove(cached["file"])
except OSError:
pass
"""清理内存缓存(磁盘缓存保留)"""
self._cache.clear()
logger.info("已清理所有缓存")
logger.info("已清理内存缓存")
# ── 内部方法 ──
@staticmethod
def _path_key(template_path: str) -> str:
"""模板路径 → 缓存文件名 key"""
return hashlib.md5(template_path.encode()).hexdigest()[:16]
def _update_index(self, template_path: str, entry: dict) -> None:
"""更新磁盘索引文件"""
index = {}
if os.path.isfile(_INDEX_FILE):
try:
with open(_INDEX_FILE, "r", encoding="utf-8") as f:
index = json.load(f)
except (json.JSONDecodeError, OSError):
pass
index.setdefault("templates", {})[template_path] = {
"file": entry["file"],
"layout": entry.get("layout", "A4"),
"texts": entry.get("texts", {}),
"extent": (
{
"xmin": entry["extent"].xMinimum(),
"ymin": entry["extent"].yMinimum(),
"xmax": entry["extent"].xMaximum(),
"ymax": entry["extent"].yMaximum(),
}
if entry.get("extent") else None
),
}
with open(_INDEX_FILE, "w", encoding="utf-8") as f:
json.dump(index, f, ensure_ascii=False, indent=2)