From 6560696a3734462ead1ad21ab32b30f829346368 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 8 Feb 2026 19:57:44 +0800 Subject: [PATCH] feat: Implement multi-source weather data collection from OpenWeatherMap, Visual Crossing, and NOAA METAR, and add NYC market-specific data handling. --- bot_listener.py | 24 ++++++++++------ save_nyc_mb.py | 17 +++++++++++ src/data_collection/weather_sources.py | 32 +++++++++++++++++---- test_nyc_mb.py | 39 ++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 save_nyc_mb.py create mode 100644 test_nyc_mb.py diff --git a/bot_listener.py b/bot_listener.py index 54114665..1f755f13 100644 --- a/bot_listener.py +++ b/bot_listener.py @@ -218,13 +218,13 @@ def start_bot(): "la": "los angeles", "洛杉矶": "los angeles", } - # 1. 尝试直接从映射表获取 + # 1. 第一优先级:严格全字匹配 city_name = STANDARD_MAPPING.get(city_input) - # 2. 如果没匹配到,尝试前缀匹配 (如输入 "seou") - if not city_name: + # 2. 第二优先级:如果长度 >= 3,尝试前缀匹配 + if not city_name and len(city_input) >= 3: for k, v in STANDARD_MAPPING.items(): - if len(city_input) >= 3 and k.startswith(city_input[:3]): + if k.startswith(city_input): city_name = v break @@ -278,19 +278,27 @@ def start_bot(): if mb_high is not None: sources.append("MB") - comp_parts.append(f"MB: {mb_high}") + comp_parts.append(f"MB: {mb_high:.1f}{temp_symbol}" if isinstance(mb_high, (int, float)) else f"MB: {mb_high}") if nws_high is not None: sources.append("NWS") - comp_parts.append(f"NWS: {nws_high}") + comp_parts.append(f"NWS: {nws_high:.1f}{temp_symbol}" if isinstance(nws_high, (int, float)) else f"NWS: {nws_high}") if mgm_high is not None: sources.append("MGM") - comp_parts.append(f"MGM: {mgm_high}") + comp_parts.append(f"MGM: {mgm_high:.1f}{temp_symbol}" if isinstance(mgm_high, (int, float)) else f"MGM: {mgm_high}") + + # 检查是否有显著分歧 (超过 5°F 或 2.5°C) + divergence_warning = "" + if mb_high is not None and max_temps: + diff = abs(mb_high - max_temps[0]) + threshold = 5.0 if temp_unit == "fahrenheit" else 2.5 + if diff > threshold: + divergence_warning = f" ⚠️ 模型显著分歧 ({diff:.1f}{temp_symbol})" comp_str = f" ({' | '.join(comp_parts)})" if comp_parts else "" sources_str = " | ".join(sources) msg_lines.append(f"\n📊 预报 ({sources_str})") - msg_lines.append(f"👉 今天: {today_t}{temp_symbol}{comp_str}") + msg_lines.append(f"👉 今天: {today_t}{temp_symbol}{comp_str}{divergence_warning}") # 明后天 if len(dates) > 1: diff --git a/save_nyc_mb.py b/save_nyc_mb.py new file mode 100644 index 00000000..36f8fd6f --- /dev/null +++ b/save_nyc_mb.py @@ -0,0 +1,17 @@ +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() diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index 2ebd07c2..5accfbd6 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -535,27 +535,47 @@ class WeatherDataCollector: 从 Meteoblue 网页抓取多模型预测数据的“共识”最高温 """ try: + # 1. 构造精确的方向后缀 URL + lat_dir = "N" if lat >= 0 else "S" + lon_dir = "E" if lon >= 0 else "W" + # Meteoblue 坐标 URL 习惯:保留 3 位小数,使用大写方向后缀 + coord_str = f"{abs(lat):.3f}{lat_dir}{abs(lon):.3f}{lon_dir}" tz_slug = timezone_name.replace("/", "%2F") - url = f"https://www.meteoblue.com/en/weather/week/{lat}N{lon}E8_{tz_slug}" + + # 使用 forecast/week 路径通常比直接 week 更稳定 + url = f"https://www.meteoblue.com/en/weather/week/{coord_str}" + 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" + "Accept-Language": "en-US,en;q=0.9", + "Referer": "https://www.meteoblue.com/" } response = self.session.get( url, headers=headers, - timeout=self.timeout + timeout=self.timeout, + allow_redirects=True # 允许重定向,但我们要检查终点 ) response.raise_for_status() content = response.text + + # 2. 检查是否重定向到了错误的地方 (例如大阪 Osaka) + # 如果 URL 里有 W 但页面内容里全是 E 或者城市名完全对不上 + if lon < 0 and "W" in coord_str: + 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": url, + "url": response.url, "today_high": None, "daily_highs": [], "unit": "celsius" @@ -568,7 +588,7 @@ class WeatherDataCollector: val = float(match.group(1)) result["today_high"] = c_to_f(val) if use_fahrenheit else val - # 同时提取接下来几天的最高温 + # 同时提取接下来几天 all_highs = re.findall(r'tab-temp-max.*?(\d+) °C', content, re.DOTALL) if all_highs: if use_fahrenheit: @@ -577,7 +597,7 @@ class WeatherDataCollector: result["daily_highs"] = [float(h) for h in all_highs] result["unit"] = "fahrenheit" if use_fahrenheit else "celsius" - logger.info(f"✅ Meteoblue 抓取成功: 今天 {result['today_high']}{result['unit']}") + logger.info(f"✅ Meteoblue 抓取成功 ({coord_str}): 今天 {result['today_high']}{result['unit']}") return result except Exception as e: logger.error(f"Meteoblue fetch failed: {e}") diff --git a/test_nyc_mb.py b/test_nyc_mb.py new file mode 100644 index 00000000..543d6834 --- /dev/null +++ b/test_nyc_mb.py @@ -0,0 +1,39 @@ +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()