Files
PolyWeather/src/data_collection/nmc_sources.py
T
2026-04-08 06:53:49 +08:00

209 lines
8.3 KiB
Python

from __future__ import annotations
import re
import time
from datetime import datetime
from typing import Any, Dict, List, Optional
from loguru import logger
from src.utils.metrics import record_source_call
NMC_CITY_REFERENCES: Dict[str, Dict[str, Any]] = {
"shanghai": {
"region_label": "浦东",
"page_url": "https://m.nmc.cn/publish/forecast/ASH/pudong.html",
"station_code": "atcMf",
},
"beijing": {
"region_label": "顺义",
"page_url": "https://m.nmc.cn/publish/forecast/ABJ/shunyi.html",
"station_code": "MKoqG",
},
"chongqing": {
"region_label": "渝北",
"page_url": "https://m.nmc.cn/publish/forecast/ACQ/yubei.html",
"station_code": "xFVYU",
},
"chengdu": {
"region_label": "双流",
"page_url": "https://m.nmc.cn/publish/forecast/ASC/shuangliu.html",
"station_code": "grFhZ",
},
"wuhan": {
"region_label": "武汉",
"page_url": "https://m.nmc.cn/publish/forecast/AHB/wuhan.html",
"station_code": "bSpCz",
},
"shenzhen": {
"region_label": "深圳",
"page_url": "https://m.nmc.cn/publish/forecast/AGD/shenzuo.html",
"station_code": "AhpEU",
},
}
class NmcSourceMixin:
def _nmc_http_get(self, url: str):
getter = getattr(self, "_http_get", None)
if callable(getter):
return getter(url)
return self.session.get(url, timeout=self.timeout)
def _nmc_http_get_json(self, url: str):
getter = getattr(self, "_http_get_json", None)
if callable(getter):
return getter(url)
response = self.session.get(url, timeout=self.timeout)
response.raise_for_status()
return response.json()
@staticmethod
def _nmc_optional_text(value: Any) -> Optional[str]:
text = str(value or "").strip()
if text in ("", "9999"):
return None
return text
@staticmethod
def _nmc_optional_float(value: Any) -> Optional[float]:
if value in (None, "", "9999", 9999, 9999.0):
return None
try:
return float(value)
except Exception:
return None
def _resolve_nmc_station_code(self, city: str) -> Optional[str]:
city_key = str(city or "").strip().lower()
meta = NMC_CITY_REFERENCES.get(city_key) or {}
station_code = str(meta.get("station_code") or "").strip()
if station_code:
return station_code
page_url = str(meta.get("page_url") or "").strip()
if not page_url:
return None
try:
resp = self._nmc_http_get(page_url)
resp.raise_for_status()
match = re.search(
r"renderWeatherRealPanel\('([^']+)',\s*'([^']+)'\)",
resp.text,
)
if not match:
return None
station_code = str(match.group(1) or "").strip()
if station_code:
meta["station_code"] = station_code
return station_code
except Exception as exc:
logger.warning("NMC station code resolve failed city={} error={}", city_key, exc)
return None
def fetch_nmc_region_current(
self,
city: str,
use_fahrenheit: bool = False,
) -> Optional[Dict[str, Any]]:
started = time.perf_counter()
city_key = str(city or "").strip().lower()
meta = NMC_CITY_REFERENCES.get(city_key) or {}
if not meta:
record_source_call("nmc", "current", "unsupported_city", (time.perf_counter() - started) * 1000.0)
return None
cache_key = f"{city_key}:{use_fahrenheit}"
now_ts = time.time()
with self._nmc_cache_lock:
cached = self._nmc_cache.get(cache_key)
if cached and now_ts - cached["t"] < self.nmc_cache_ttl_sec:
record_source_call("nmc", "current", "cache_hit", (time.perf_counter() - started) * 1000.0)
return cached["d"]
station_code = self._resolve_nmc_station_code(city_key)
if not station_code:
record_source_call("nmc", "current", "missing_station_code", (time.perf_counter() - started) * 1000.0)
return None
try:
url = f"https://www.nmc.cn/rest/real/{station_code}"
payload = self._nmc_http_get_json(url)
if not isinstance(payload, dict) or not isinstance(payload.get("weather"), dict):
record_source_call("nmc", "current", "empty", (time.perf_counter() - started) * 1000.0)
return None
weather = payload.get("weather") or {}
temp_c = weather.get("temperature")
if temp_c in (None, "", "9999"):
record_source_call("nmc", "current", "no_temperature", (time.perf_counter() - started) * 1000.0)
return None
temp_c = float(temp_c)
temp = round(temp_c * 9 / 5 + 32, 1) if use_fahrenheit else round(temp_c, 1)
station = payload.get("station") or {}
result = {
"source": "nmc",
"timestamp": datetime.utcnow().isoformat(),
"station_code": station_code,
"station_name": station.get("city") or meta.get("region_label") or city_key.title(),
"page_url": meta.get("page_url"),
"publish_time": payload.get("publish_time"),
"current": {
"temp": temp,
"humidity": self._nmc_optional_float(weather.get("humidity")),
"rain": self._nmc_optional_float(weather.get("rain")),
"airpressure": self._nmc_optional_float(weather.get("airpressure")),
"wx_desc": weather.get("info"),
"wind_direction": self._nmc_optional_text((payload.get("wind") or {}).get("direct")),
"wind_power": self._nmc_optional_text((payload.get("wind") or {}).get("power")),
},
}
with self._nmc_cache_lock:
self._nmc_cache[cache_key] = {"d": result, "t": now_ts}
record_source_call("nmc", "current", "success", (time.perf_counter() - started) * 1000.0)
return result
except Exception as exc:
logger.warning("NMC current fetch failed city={} code={} error={}", city_key, station_code, exc)
with self._nmc_cache_lock:
stale = self._nmc_cache.get(cache_key)
if stale:
record_source_call("nmc", "current", "stale_cache", (time.perf_counter() - started) * 1000.0)
return stale["d"]
record_source_call("nmc", "current", "error", (time.perf_counter() - started) * 1000.0)
return None
def fetch_nmc_official_nearby(
self,
city: str,
use_fahrenheit: bool = False,
) -> List[Dict[str, Any]]:
current = self.fetch_nmc_region_current(city, use_fahrenheit=use_fahrenheit)
if not current:
return []
meta = NMC_CITY_REFERENCES.get(str(city or "").strip().lower()) or {}
city_meta = self.CITY_REGISTRY.get(str(city or "").strip().lower()) or {}
return [
{
"name": f"{meta.get('region_label') or current.get('station_name')}区域实况 (NMC)",
"station_label": f"{meta.get('region_label') or current.get('station_name')}区域实况 (NMC)",
"lat": city_meta.get("lat"),
"lon": city_meta.get("lon"),
"temp": current.get("current", {}).get("temp"),
"istNo": current.get("station_code"),
"icao": current.get("station_code"),
"source": "nmc",
"source_label": "NMC",
"obs_time": current.get("publish_time"),
"page_url": current.get("page_url"),
"humidity": current.get("current", {}).get("humidity"),
"rain": current.get("current", {}).get("rain"),
"airpressure": current.get("current", {}).get("airpressure"),
"wx_desc": current.get("current", {}).get("wx_desc"),
"wind_direction_text": current.get("current", {}).get("wind_direction"),
"wind_power_text": current.get("current", {}).get("wind_power"),
}
]