# external_apis.py import requests import json import logging import random from typing import Dict, Tuple, Optional import time class WeatherAPI: def __init__(self): self.logger = logging.getLogger(__name__) # 使用腾讯天气API self.base_url = "https://wis.qq.com/weather/common" def parse_city(self, city_string: str) -> Tuple[str, str, str]: """ 解析城市字符串,返回省份、城市、区县 参数: city_string: 完整的地址字符串 返回: (province, city, county) """ # 匹配省份或自治区 province_regex = r"(.*?)(省|自治区)" # 匹配城市或州 city_regex = r"(.*?省|.*?自治区)(.*?市|.*?州)" # 匹配区、县或镇 county_regex = r"(.*?市|.*?州)(.*?)(区|县|镇)" province = "" city = "" county = "" import re # 先尝试匹配省份或自治区 province_match = re.search(province_regex, city_string) if province_match: province = province_match.group(1).strip() # 然后尝试匹配城市或州 city_match = re.search(city_regex, city_string) if city_match: city = city_match.group(2).strip() else: # 如果没有匹配到城市,则可能是直辖市或者直接是区/县 city = city_string # 最后尝试匹配区、县或镇 county_match = re.search(county_regex, city_string) if county_match: county = county_match.group(2).strip() # 如果有区、县或镇,那么前面的城市部分需要重新解析 if city_match: city = city_match.group(2).strip() # 特殊情况处理,去除重复的省市名称 if city and province and city.startswith(province): city = city.replace(province, "").strip() if county and city and county.startswith(city): county = county.replace(city, "").strip() # 去除后缀 city = city.rstrip('市州') if county: county = county.rstrip('区县镇') # self.logger.info(f"解析结果 - 省份: {province}, 城市: {city}, 区县: {county}") return province, city, county def get_weather_by_qq_api(self, province: str, city: str, county: str) -> Optional[Dict]: """ 使用腾讯天气API获取天气信息 参数: province: 省份 city: 城市 county: 区县 返回: 天气信息字典 or None """ try: params = { 'source': 'pc', 'weather_type': 'observe', 'province': province, 'city': city, 'county': county } response = requests.get(self.base_url, params=params, timeout=10) response.raise_for_status() data = response.json() if data.get('status') == 200: observe_data = data.get('data', {}).get('observe', {}) return { 'weather': observe_data.get('weather', ''), 'temperature': observe_data.get('degree', ''), 'pressure': observe_data.get('pressure', '1013') } else: self.logger.error(f"腾讯天气API错误: {data.get('message')}") return None except Exception as e: self.logger.error(f"腾讯天气API调用失败: {str(e)}") return None def normalize_weather_text(self, weather_text: str) -> str: """ 将天气描述标准化为: 晴;阴;雨;雪;风;其他 参数: weather_text: 原始天气描述 返回: 标准化后的天气文本 """ if not weather_text: return '其他' weather_text_lower = weather_text.lower() # 晴 if any(word in weather_text_lower for word in ['晴', 'sunny', 'clear']): return '晴' # 阴 elif any(word in weather_text_lower for word in ['阴', '多云', 'cloudy', 'overcast']): return '阴' # 雨 elif any(word in weather_text_lower for word in ['雨', 'rain', 'drizzle', 'shower']): return '雨' # 雪 elif any(word in weather_text_lower for word in ['雪', 'snow']): return '雪' # 风 elif any(word in weather_text_lower for word in ['风', 'wind']): return '风' # 其他 else: return '其他' def adjust_pressure(self, pressure: float) -> float: """ 调整气压值:低于700时,填700-750之间随机一个;高于700就按实际情况填 参数: pressure: 原始气压值 返回: 调整后的气压值 """ try: pressure_float = float(pressure) if pressure_float < 700: adjusted_pressure = random.randint(700, 750) self.logger.info(f"气压值 {pressure_float} 低于700,调整为: {adjusted_pressure:.1f}") return round(adjusted_pressure, 1) else: self.logger.info(f"使用实际气压值: {pressure_float}") return round(pressure_float, 1) except (ValueError, TypeError): self.logger.warning(f"气压值格式错误: {pressure},使用默认值720") return round(random.randint(700, 750), 1) def get_weather_by_address(self, address: str, max_retries: int = 2) -> Optional[Dict]: """ 根据地址获取天气信息 参数: address: 地址字符串 max_retries: 最大重试次数 返回: { 'weather': '晴/阴/雨/雪/风/其他', 'temperature': 温度值, 'pressure': 气压值 } or None """ # self.logger.info(f"开始获取地址 '{address}' 的天气信息") # 首先解析地址 province, city, county = self.parse_city(address) if not province and not city: self.logger.error("无法解析地址") return self.get_fallback_weather() # 获取天气信息 weather_data = None for attempt in range(max_retries): try: # self.logger.info(f"尝试获取天气信息 (第{attempt + 1}次)") weather_data = self.get_weather_by_qq_api(province, city, county) if weather_data: break time.sleep(1) # 短暂延迟后重试 except Exception as e: self.logger.warning(f"第{attempt + 1}次尝试失败: {str(e)}") time.sleep(1) if not weather_data: self.logger.warning("获取天气信息失败,使用备用数据") return self.get_fallback_weather() # 处理天气数据 try: # 标准化天气文本 normalized_weather = self.normalize_weather_text(weather_data['weather']) # 调整气压值 adjusted_pressure = self.adjust_pressure(weather_data['pressure']) # 处理温度 temperature = float(weather_data['temperature']) result = { 'weather': normalized_weather, 'temperature': round(temperature, 1), 'pressure': adjusted_pressure } self.logger.info(f"成功获取天气信息: {result}") return result except Exception as e: self.logger.error(f"处理天气数据时出错: {str(e)}") return self.get_fallback_weather() def get_fallback_weather(self) -> Dict: """ 获取备用天气数据(当所有API都失败时使用) 返回: 默认天气数据 """ self.logger.info("使用备用天气数据") return { 'weather': '阴', 'temperature': round(random.randint(15, 30), 1), 'pressure': round(random.randint(700, 750), 1) } def get_weather_simple(self, address: str) -> Tuple[str, float, float]: """ 简化接口:直接返回天气、温度、气压 参数: address: 地址字符串 返回: (weather, temperature, pressure) """ weather_data = self.get_weather_by_address(address) if weather_data: return weather_data['weather'], weather_data['temperature'], weather_data['pressure'] else: fallback = self.get_fallback_weather() return fallback['weather'], fallback['temperature'], fallback['pressure'] # 创建全局实例 weather_api = WeatherAPI() # 直接可用的函数 def get_weather_by_address(address: str) -> Optional[Dict]: """ 根据地址获取天气信息(直接调用函数) 参数: address: 地址字符串 返回: { 'weather': '晴/阴/雨/雪/风/其他', 'temperature': 温度值, 'pressure': 气压值 } or None """ return weather_api.get_weather_by_address(address) def get_weather_simple(address: str) -> Tuple[str, float, float]: """ 简化接口:直接返回天气、温度、气压 参数: address: 地址字符串 返回: (weather, temperature, pressure) """ return weather_api.get_weather_simple(address)