refactor: Implement structured data collection and configuration modules, replacing legacy city-specific files and updating documentation.
This commit is contained in:
@@ -8,6 +8,7 @@ An intelligent weather information bot designed to provide ultra-fast, live mete
|
|||||||
|
|
||||||
- **Python 3.11+**
|
- **Python 3.11+**
|
||||||
- Dependencies: `pip install -r requirements.txt`
|
- Dependencies: `pip install -r requirements.txt`
|
||||||
|
- **Environment**: Configure `METEOBLUE_API_KEY` in `.env` to enable high-precision London forecasts.
|
||||||
|
|
||||||
### Running Locally (Windows/Linux)
|
### Running Locally (Windows/Linux)
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ graph TD
|
|||||||
|
|
||||||
subgraph "Data Engine"
|
subgraph "Data Engine"
|
||||||
Collector --> OM[Open-Meteo API]
|
Collector --> OM[Open-Meteo API]
|
||||||
Collector --> MB[Meteoblue Scraper]
|
Collector --> MB[Meteoblue Weather API]
|
||||||
Collector --> NOAA[METAR Data Center]
|
Collector --> NOAA[METAR Data Center]
|
||||||
Collector --> MGM[Turkish MGM API]
|
Collector --> MGM[Turkish MGM API]
|
||||||
Collector --> NWS[US NWS API]
|
Collector --> NWS[US NWS API]
|
||||||
|
|||||||
+2
-1
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
- **Python 3.11+**
|
- **Python 3.11+**
|
||||||
- 依赖安装: `pip install -r requirements.txt`
|
- 依赖安装: `pip install -r requirements.txt`
|
||||||
|
- **环境变量**: 需在 `.env` 中配置 `METEOBLUE_API_KEY` 以激活伦敦高精度预报。
|
||||||
|
|
||||||
### 本地运行 (Windows/Linux)
|
### 本地运行 (Windows/Linux)
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ graph TD
|
|||||||
|
|
||||||
subgraph "Data Engine"
|
subgraph "Data Engine"
|
||||||
Collector --> OM[Open-Meteo API]
|
Collector --> OM[Open-Meteo API]
|
||||||
Collector --> MB[Meteoblue Scraper]
|
Collector --> MB[Meteoblue Weather API]
|
||||||
Collector --> NOAA[METAR Data Center]
|
Collector --> NOAA[METAR Data Center]
|
||||||
Collector --> MGM[Turkish MGM API]
|
Collector --> MGM[Turkish MGM API]
|
||||||
Collector --> NWS[US NWS API]
|
Collector --> NWS[US NWS API]
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import requests
|
|
||||||
|
|
||||||
def save_nyc_mb():
|
|
||||||
url = "https://www.meteoblue.com/en/weather/week/40.713N74.006W"
|
|
||||||
headers = {
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
||||||
}
|
|
||||||
proxies = {"http": "http://127.0.0.1:7890", "https": "http://127.0.0.1:7890"}
|
|
||||||
try:
|
|
||||||
resp = requests.get(url, headers=headers, proxies=proxies, timeout=10)
|
|
||||||
with open("nyc_mb.html", "w", encoding="utf-8") as f:
|
|
||||||
f.write(resp.text)
|
|
||||||
print("Saved nyc_mb.html")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
|
|
||||||
save_nyc_mb()
|
|
||||||
@@ -37,7 +37,9 @@ class WeatherDataCollector:
|
|||||||
|
|
||||||
def __init__(self, config: dict):
|
def __init__(self, config: dict):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.wunderground_key = config.get("wunderground_api_key")
|
weather_cfg = config.get("weather", {})
|
||||||
|
self.wunderground_key = weather_cfg.get("wunderground_api_key")
|
||||||
|
self.meteoblue_key = weather_cfg.get("meteoblue_api_key")
|
||||||
|
|
||||||
self.timeout = 30 # 增加超时以支持高延迟 VPS
|
self.timeout = 30 # 增加超时以支持高延迟 VPS
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
@@ -532,75 +534,65 @@ class WeatherDataCollector:
|
|||||||
use_fahrenheit: bool = False,
|
use_fahrenheit: bool = False,
|
||||||
) -> Optional[Dict]:
|
) -> Optional[Dict]:
|
||||||
"""
|
"""
|
||||||
从 Meteoblue 网页抓取多模型预测数据的“共识”最高温
|
通过 Meteoblue 官方 API 获取高精度预测数据
|
||||||
"""
|
"""
|
||||||
|
if not self.meteoblue_key:
|
||||||
|
logger.warning("Meteoblue API Key 未配置,跳过抓取。")
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. 构造精确的方向后缀 URL
|
# 1. 调用官方 API (使用 basic-day 包,它是多模型 ML 融合结果)
|
||||||
lat_dir = "N" if lat >= 0 else "S"
|
# 格式: https://my.meteoblue.com/packages/basic-day?apikey=KEY&lat=LAT&lon=LON&format=json
|
||||||
lon_dir = "E" if lon >= 0 else "W"
|
url = "https://my.meteoblue.com/packages/basic-day"
|
||||||
# Meteoblue 坐标 URL 习惯:保留 3 位小数,使用大写方向后缀
|
params = {
|
||||||
coord_str = f"{abs(lat):.3f}{lat_dir}{abs(lon):.3f}{lon_dir}"
|
"apikey": self.meteoblue_key,
|
||||||
tz_slug = timezone_name.replace("/", "%2F")
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
# 使用 forecast/week 路径通常比直接 week 更稳定
|
"format": "json",
|
||||||
url = f"https://www.meteoblue.com/en/weather/week/{coord_str}"
|
"as_daylight": "true"
|
||||||
if tz_slug:
|
|
||||||
url += f"_{tz_slug}"
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
||||||
"Accept-Language": "en-US,en;q=0.9",
|
|
||||||
"Referer": "https://www.meteoblue.com/"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.session.get(
|
response = self.session.get(
|
||||||
url,
|
url,
|
||||||
headers=headers,
|
params=params,
|
||||||
timeout=self.timeout,
|
timeout=self.timeout
|
||||||
allow_redirects=True # 允许重定向,但我们要检查终点
|
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
content = response.text
|
day_data = data.get("data_day", {})
|
||||||
|
max_temps = day_data.get("temperature_max", [])
|
||||||
|
|
||||||
# 2. 检查是否重定向到了错误的地方 (例如大阪 Osaka)
|
if not max_temps:
|
||||||
# 如果 URL 里有 W 但页面内容里全是 E 或者城市名完全对不上
|
logger.warning(f"Meteoblue API 返回数据中找不到最高温 (坐标: {lat},{lon})")
|
||||||
if lon < 0 and "W" in coord_str:
|
return None
|
||||||
if "osaka" in response.url.lower():
|
|
||||||
logger.warning(f"⚠️ Meteoblue 重定向异常: 目标 {coord_str} 却返回了 {response.url}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 3. 提取最高温
|
|
||||||
match = re.search(r'Today.*?tab-temp-max.*?(\d+) °C', content, re.DOTALL | re.IGNORECASE)
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"source": "meteoblue",
|
|
||||||
"url": response.url,
|
|
||||||
"today_high": None,
|
|
||||||
"daily_highs": [],
|
|
||||||
"unit": "celsius"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
# 2. 转换单位
|
||||||
def c_to_f(c):
|
def c_to_f(c):
|
||||||
return round((c * 9/5) + 32, 1)
|
return round((c * 9/5) + 32, 1)
|
||||||
|
|
||||||
if match:
|
result = {
|
||||||
val = float(match.group(1))
|
"source": "meteoblue",
|
||||||
result["today_high"] = c_to_f(val) if use_fahrenheit else val
|
"today_high": None,
|
||||||
|
"daily_highs": [],
|
||||||
|
"unit": "fahrenheit" if use_fahrenheit else "celsius",
|
||||||
|
"url": f"https://www.meteoblue.com/en/weather/week/{lat}N{lon}E" # 仅供参考
|
||||||
|
}
|
||||||
|
|
||||||
# 同时提取接下来几天
|
# 提取今日最高
|
||||||
all_highs = re.findall(r'tab-temp-max.*?(\d+) °C', content, re.DOTALL)
|
mb_today_c = max_temps[0]
|
||||||
if all_highs:
|
result["today_high"] = c_to_f(mb_today_c) if use_fahrenheit else mb_today_c
|
||||||
if use_fahrenheit:
|
|
||||||
result["daily_highs"] = [c_to_f(float(h)) for h in all_highs]
|
|
||||||
else:
|
|
||||||
result["daily_highs"] = [float(h) for h in all_highs]
|
|
||||||
|
|
||||||
result["unit"] = "fahrenheit" if use_fahrenheit else "celsius"
|
# 提取接下来几天的最高温
|
||||||
logger.info(f"✅ Meteoblue 抓取成功 ({coord_str}): 今天 {result['today_high']}{result['unit']}")
|
if use_fahrenheit:
|
||||||
|
result["daily_highs"] = [c_to_f(t) for t in max_temps]
|
||||||
|
else:
|
||||||
|
result["daily_highs"] = max_temps
|
||||||
|
|
||||||
|
logger.info(f"✅ Meteoblue API 获取成功 ({lat},{lon}): 今天 {result['today_high']}{result['unit']}")
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Meteoblue fetch failed: {e}")
|
logger.error(f"Meteoblue API fetch failed: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def extract_date_from_title(self, title: str) -> Optional[str]:
|
def extract_date_from_title(self, title: str) -> Optional[str]:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ def load_config():
|
|||||||
"openweather_api_key": get_env_or_none("OPENWEATHER_API_KEY"),
|
"openweather_api_key": get_env_or_none("OPENWEATHER_API_KEY"),
|
||||||
"wunderground_api_key": get_env_or_none("WUNDERGROUND_API_KEY"),
|
"wunderground_api_key": get_env_or_none("WUNDERGROUND_API_KEY"),
|
||||||
"visualcrossing_api_key": get_env_or_none("VISUALCROSSING_API_KEY"),
|
"visualcrossing_api_key": get_env_or_none("VISUALCROSSING_API_KEY"),
|
||||||
|
"meteoblue_api_key": get_env_or_none("METEOBLUE_API_KEY"),
|
||||||
"proxy": os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY"),
|
"proxy": os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY"),
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import requests
|
|
||||||
import re
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
def test_mb_nyc():
|
|
||||||
lat = 40.7128
|
|
||||||
lon = -74.0060
|
|
||||||
timezone_name = "America/New_York"
|
|
||||||
|
|
||||||
lat_dir = "N" if lat >= 0 else "S"
|
|
||||||
lon_dir = "E" if lon >= 0 else "W"
|
|
||||||
tz_slug = timezone_name.replace("/", "%2F")
|
|
||||||
|
|
||||||
# Try both ways
|
|
||||||
url1 = f"https://www.meteoblue.com/en/weather/week/{lat}N{lon}E8_{tz_slug}"
|
|
||||||
url2 = f"https://www.meteoblue.com/en/weather/week/{abs(lat)}{lat_dir}{abs(lon)}{lon_dir}8_{tz_slug}"
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
||||||
"Accept-Language": "en-US,en;q=0.9"
|
|
||||||
}
|
|
||||||
proxies = {"http": "http://127.0.0.1:7890", "https": "http://127.0.0.1:7890"}
|
|
||||||
|
|
||||||
for url in [url1, url2]:
|
|
||||||
print(f"\nTesting URL: {url}")
|
|
||||||
try:
|
|
||||||
resp = requests.get(url, headers=headers, proxies=proxies, timeout=10)
|
|
||||||
print(f"Status: {resp.status_code}")
|
|
||||||
content = resp.text
|
|
||||||
match = re.search(r'Today.*?tab-temp-max.*?(\d+) °C', content, re.DOTALL | re.IGNORECASE)
|
|
||||||
if match:
|
|
||||||
print(f"Captured High: {match.group(1)}°C")
|
|
||||||
else:
|
|
||||||
all_highs = re.findall(r'tab-temp-max.*?(\d+) °C', content, re.DOTALL)
|
|
||||||
print(f"All Highs Found: {all_highs}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
|
|
||||||
test_mb_nyc()
|
|
||||||
Reference in New Issue
Block a user