diff --git a/skills/weather/.claude-plugin/plugin.json b/skills/weather/.claude-plugin/plugin.json new file mode 100644 index 0000000..922d903 --- /dev/null +++ b/skills/weather/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "weather", + "description": "提供中国天气预报查询功能,支持全国300+城市的当前天气和未来1/3/7/15/40天天气预报查询。", + "mcpServers": { + "weather": { + "transport": "stdio", + "command": "python", + "args": [ + "./skills/weather/mcp.py" + ] + } + } +} diff --git a/skills/weather/SKILL.md b/skills/weather/SKILL.md new file mode 100644 index 0000000..4b24098 --- /dev/null +++ b/skills/weather/SKILL.md @@ -0,0 +1,149 @@ +--- +name: weather +description: 提供中国天气预报查询功能,支持全国300+城市的当前天气和未来1/3/7/15/40天天气预报查询。当需要查询天气情况、温度、风向等信息时使用。 +--- + +# 天气预报查询 (Weather) + +## 功能概述 + +天气预报查询 skill 提供中国天气网数据查询服务,支持全国城市天气查询、当前天气查询和城市搜索功能。 + +## 命令行调用 + +```bash +# 查询杭州7天天气预报 +python scripts/weather_query.py query 杭州 + +# 查询北京3天天气预报 +python scripts/weather_query.py query 北京 --days 3 + +# 查询深圳当前天气 +python scripts/weather_query.py query 深圳 --current + +# 搜索带"州"字的城市 +python scripts/weather_query.py search 州 + +# 输出JSON格式的天气数据 +python scripts/weather_query.py json 杭州 --days 3 +``` + +## 命令列表 + +| 命令 | 功能 | 说明 | +|-----|------|-----| +| `query` | 天气预报查询 | 查询指定城市的天气预报或当前天气 | +| `search` | 城市搜索 | 根据关键词搜索城市 | +| `json` | JSON输出 | 输出JSON格式的天气数据 | + +## 参数说明 + +### query 命令参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `city` | 是 | 城市名称,如"杭州"、"北京"、"上海" | +| `--days` | 否 | 查询天数,可选1/3/7/15/40,默认7天 | +| `--current`, `-c` | 否 | 查询当前天气而非预报 | + +### search 命令参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `keyword` | 是 | 搜索关键词 | +| `--limit` | 否 | 返回结果数量限制,默认20 | + +### json 命令参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `city` | 是 | 城市名称 | +| `--days` | 否 | 查询天数,默认7天 | + +## 返回数据格式 + +### 天气预报文本输出示例 + +``` +📍 杭州 天气预报 +============================== + +📅 15日(今天) (今天) + 🌤️ 晴 + 🌡️ 21/8℃ + 💨 东北风 <3级 + +📅 16日(明天) + 🌤️ 晴转多云 + 🌡️ 21/9℃ + 💨 东风 <3级 + +📅 17日(后天) + 🌤️ 小雨转多云 + 🌡️ 13/9℃ + 💨 东风 <3级 + +⏰ 更新时间: 07:30 +``` + +### JSON输出示例 + +```json +{ + "city": "杭州", + "update_time": "07:30", + "forecast": [ + { + "date": "15日(今天)", + "weather": "晴", + "temperature": "21/8℃", + "wind": "东北风 <3级" + }, + { + "date": "16日(明天)", + "weather": "晴转多云", + "temperature": "21/9℃", + "wind": "东风 <3级" + }, + { + "date": "17日(后天)", + "weather": "小雨转多云", + "temperature": "13/9℃", + "wind": "东风 <3级" + } + ] +} +``` + +## 支持的城市 + +覆盖全国34个省级行政区的300+主要城市,包括: + +- **直辖市**: 北京、上海、天津、重庆 +- **省会城市**: 广州、成都、武汉、西安、南京等 +- **地级市**: 深圳、宁波、青岛、大连等 +- **区县级**: 杭州下辖的萧山、余杭、临安等 + +使用 `search` 命令可查询完整城市列表。 + +## 城市编码规则 + +城市编码格式:`101XXYYZZ` + +| 部分 | 说明 | 示例 | +|------|------|------| +| XX | 省份代码 (01-34) | 浙江=21 | +| YY | 城市代码 | 杭州=01 | +| ZZ | 区县代码 | 城区=01 | + +示例: +- 杭州:101210101 +- 萧山:101210102 +- 北京:101010100 + +## 约束条件 + +- 数据来源于中国天气网 (www.weather.com.cn) +- 数据仅供参考,请以官方发布为准 +- 每日更新时间约为07:30和11:00 +- 支持1天、3天、7天、15天、40天预报查询 diff --git a/skills/weather/mcp.py b/skills/weather/mcp.py new file mode 100644 index 0000000..4d38d77 --- /dev/null +++ b/skills/weather/mcp.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +天气查询MCP服务器 +提供中国天气网天气预报查询功能 +""" + +import asyncio +import json +import sys +import os +from typing import Any, Dict + +# 将 scripts 目录加入 sys.path,以便导入 weather 模块 +SCRIPTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "scripts") +if SCRIPTS_DIR not in sys.path: + sys.path.insert(0, SCRIPTS_DIR) + +# 将 mcp 目录加入 sys.path,以便导入 mcp_common +MCP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "mcp") +MCP_DIR = os.path.abspath(MCP_DIR) +if MCP_DIR not in sys.path: + sys.path.insert(0, MCP_DIR) + +from mcp_common import ( + create_error_response, + create_initialize_response, + create_ping_response, + create_tools_list_response, + handle_mcp_streaming, +) + +from weather import query_weather, find_city_code + + +# MCP 工具定义 +WEATHER_TOOLS = [ + { + "name": "weather_query", + "description": "查询指定城市的天气预报,支持1/3/7/15/40天预报。返回格式化的天气文本,包含日期、天气状况、温度、风向等信息。", + "inputSchema": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "城市名称,如:杭州、北京、上海、深圳" + }, + "days": { + "type": "integer", + "description": "查询天数,可选1/3/7/15/40,默认7天", + "enum": [1, 3, 7, 15, 40], + "default": 7 + } + }, + "required": ["city"] + } + } +] + + +def handle_weather_query(city: str, days: int = 7) -> Dict[str, Any]: + """处理天气预报查询""" + try: + city_code = find_city_code(city) + if not city_code: + return { + "content": [{"type": "text", "text": f"未找到城市 \"{city}\""}] + } + + result = query_weather(city, days) + if result: + return {"content": [{"type": "text", "text": result}]} + else: + return {"content": [{"type": "text", "text": f"无法获取 {city} 的天气预报"}]} + + except Exception as e: + return {"content": [{"type": "text", "text": f"查询天气预报出错: {str(e)}"}]} + + +async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]: + """Handle MCP request""" + try: + method = request.get("method") + params = request.get("params", {}) + request_id = request.get("id") + + if method == "initialize": + return create_initialize_response(request_id, "weather-query") + + elif method == "ping": + return create_ping_response(request_id) + + elif method == "tools/list": + return create_tools_list_response(request_id, WEATHER_TOOLS) + + elif method == "tools/call": + tool_name = params.get("name") + arguments = params.get("arguments", {}) + + if tool_name == "weather_query": + city = arguments.get("city", "") + days = arguments.get("days", 7) + if not city: + return create_error_response(request_id, -32602, "Missing required parameter: city") + result = handle_weather_query(city, days) + return {"jsonrpc": "2.0", "id": request_id, "result": result} + + else: + return create_error_response(request_id, -32601, f"Unknown tool: {tool_name}") + + else: + return create_error_response(request_id, -32601, f"Unknown method: {method}") + + except Exception as e: + return create_error_response(request.get("id"), -32603, f"Internal error: {str(e)}") + + +async def main(): + """Main entry point.""" + await handle_mcp_streaming(handle_request) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/skills/weather/scripts/weather/__init__.py b/skills/weather/scripts/weather/__init__.py new file mode 100644 index 0000000..2d48946 --- /dev/null +++ b/skills/weather/scripts/weather/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +""" +中国天气网天气查询模块 +提供全国城市天气预报查询功能 +""" + +from .query import ( + WeatherQuery, + WeatherInfo, + query_weather, + query_current_weather, + search_city, +) + +from .city_codes import ( + CITY_CODES, + PROVINCE_CODES, + CITY_NAME_TO_CODE, + find_city_code, + get_city_name, +) + +__all__ = [ + # 查询类 + "WeatherQuery", + "WeatherInfo", + # 便捷函数 + "query_weather", + "query_current_weather", + "search_city", + # 城市编码 + "CITY_CODES", + "PROVINCE_CODES", + "CITY_NAME_TO_CODE", + "find_city_code", + "get_city_name", +] + +__version__ = "1.0.0" +__author__ = "Weather Skill" diff --git a/skills/weather/scripts/weather/city_codes.py b/skills/weather/scripts/weather/city_codes.py new file mode 100644 index 0000000..b4de88f --- /dev/null +++ b/skills/weather/scripts/weather/city_codes.py @@ -0,0 +1,525 @@ +# -*- coding: utf-8 -*- +""" +中国天气网城市编码数据库 +数据来源:www.weather.com.cn +城市编码格式:101XXYYZZ +- XX:省份代码(01-34) +- YY:城市代码 +- ZZ:区县代码 +""" + +# 省级编码 +PROVINCE_CODES = { + "10101": "北京", + "10102": "上海", + "10103": "天津", + "10104": "重庆", + "10105": "黑龙江", + "10106": "吉林", + "10107": "辽宁", + "10108": "内蒙古", + "10109": "河北", + "10110": "山西", + "10111": "陕西", + "10112": "山东", + "10113": "新疆", + "10114": "西藏", + "10115": "青海", + "10116": "甘肃", + "10117": "宁夏", + "10118": "河南", + "10119": "江苏", + "10120": "湖北", + "10121": "浙江", + "10122": "安徽", + "10123": "福建", + "10124": "江西", + "10125": "湖南", + "10126": "贵州", + "10127": "四川", + "10128": "广东", + "10129": "云南", + "10130": "广西", + "10131": "海南", + "10132": "香港", + "10133": "澳门", + "10134": "台湾", +} + +# 主要城市编码(市级主城区) +CITY_CODES = { + # 北京市 + "101010100": "北京", + # 上海市 + "101020100": "上海", + # 天津市 + "101030100": "天津", + # 重庆市 + "101040100": "重庆", + + # 浙江省 + "101210101": "杭州", + "101210102": "萧山", + "101210103": "桐庐", + "101210104": "淳安", + "101210105": "建德", + "101210106": "余杭", + "101210107": "临安", + "101210108": "富阳", + "101210201": "湖州", + "101210301": "嘉兴", + "101210401": "宁波", + "101210501": "绍兴", + "101210601": "台州", + "101210701": "温州", + "101210801": "丽水", + "101210901": "金华", + "101211001": "衢州", + "101211101": "舟山", + + # 江苏省 + "101190101": "南京", + "101190401": "无锡", + "101190501": "常州", + "101190601": "苏州", + "101190801": "南通", + "101190901": "连云港", + "101191001": "淮安", + "101191101": "盐城", + "101191201": "扬州", + "101191301": "镇江", + "101191401": "泰州", + "101191501": "宿迁", + + # 广东省 + "101280101": "广州", + "101280601": "深圳", + "101280701": "珠海", + "101280801": "汕头", + "101280901": "佛山", + "101281001": "韶关", + "101281101": "湛江", + "101281201": "肇庆", + "101281301": "江门", + "101281401": "茂名", + "101281501": "惠州", + "101281601": "梅州", + "101281701": "汕尾", + "101281801": "河源", + "101281901": "阳江", + "101282001": "清远", + "101282101": "东莞", + "101282201": "中山", + "101282301": "潮州", + "101282401": "揭阳", + "101282501": "云浮", + + # 四川省 + "101270101": "成都", + "101270201": "自贡", + "101270301": "攀枝花", + "101270401": "泸州", + "101270501": "德阳", + "101270601": "绵阳", + "101270701": "广元", + "101270801": "遂宁", + "101270901": "内江", + "101271001": "乐山", + "101271101": "南充", + "101271201": "眉山", + "101271301": "宜宾", + "101271401": "广安", + "101271501": "达州", + "101271601": "雅安", + "101271701": "巴中", + "101271801": "资阳", + "101271906": "阿坝", + "101272001": "甘孜", + "101272101": "凉山", + + # 湖北省 + "101200101": "武汉", + "101200201": "黄石", + "101200301": "十堰", + "101200401": "宜昌", + "101200501": "襄阳", + "101200601": "鄂州", + "101200701": "荆门", + "101200801": "孝感", + "101200901": "荆州", + "101201001": "黄冈", + "101201101": "咸宁", + "101201201": "随州", + "101201301": "恩施", + + # 湖南省 + "101250101": "长沙", + "101250201": "株洲", + "101250301": "湘潭", + "101250401": "衡阳", + "101250501": "邵阳", + "101250601": "岳阳", + "101250701": "常德", + "101250801": "张家界", + "101250901": "益阳", + "101251001": "郴州", + "101251101": "永州", + "101251201": "怀化", + "101251301": "娄底", + "101251401": "湘西", + + # 河南省 + "101180101": "郑州", + "101180201": "开封", + "101180301": "洛阳", + "101180401": "平顶山", + "101180501": "安阳", + "101180601": "鹤壁", + "101180701": "新乡", + "101180801": "焦作", + "101180901": "濮阳", + "101181001": "许昌", + "101181101": "漯河", + "101181201": "三门峡", + "101181301": "南阳", + "101181401": "商丘", + "101181501": "信阳", + "101181601": "周口", + "101181701": "驻马店", + "101181801": "济源", + + # 山东省 + "101120101": "济南", + "101120201": "青岛", + "101120301": "淄博", + "101120401": "枣庄", + "101120501": "东营", + "101120601": "烟台", + "101120701": "潍坊", + "101120801": "济宁", + "101120901": "泰安", + "101121001": "威海", + "101121101": "日照", + "101121201": "临沂", + "101121301": "德州", + "101121401": "聊城", + "101121501": "滨州", + "101121601": "菏泽", + + # 福建省 + "101230101": "福州", + "101230201": "厦门", + "101230301": "莆田", + "101230401": "三明", + "101230501": "泉州", + "101230601": "漳州", + "101230701": "南平", + "101230801": "龙岩", + "101230901": "宁德", + + # 安徽省 + "101220101": "合肥", + "101220201": "芜湖", + "101220301": "蚌埠", + "101220401": "淮南", + "101220501": "马鞍山", + "101220601": "淮北", + "101220701": "铜陵", + "101220801": "安庆", + "101220901": "黄山", + "101221001": "滁州", + "101221101": "阜阳", + "101221201": "宿州", + "101221301": "六安", + "101221401": "亳州", + "101221501": "池州", + "101221601": "宣城", + + # 陕西省 + "101110101": "西安", + "101110201": "铜川", + "101110301": "宝鸡", + "101110401": "咸阳", + "101110501": "渭南", + "101110601": "延安", + "101110701": "汉中", + "101110801": "榆林", + "101110901": "安康", + "101111001": "商洛", + + # 辽宁省 + "101070101": "沈阳", + "101070201": "大连", + "101070301": "鞍山", + "101070401": "抚顺", + "101070501": "本溪", + "101070601": "丹东", + "101070701": "锦州", + "101070801": "营口", + "101070901": "阜新", + "101071001": "辽阳", + "101071101": "盘锦", + "101071201": "铁岭", + "101071301": "朝阳", + "101071401": "葫芦岛", + + # 江西省 + "101240101": "南昌", + "101240201": "景德镇", + "101240301": "萍乡", + "101240401": "九江", + "101240501": "新余", + "101240601": "鹰潭", + "101240701": "赣州", + "101240801": "吉安", + "101240901": "宜春", + "101241001": "抚州", + "101241101": "上饶", + + # 云南省 + "101290101": "昆明", + "101290201": "曲靖", + "101290301": "玉溪", + "101290401": "保山", + "101290501": "昭通", + "101290601": "丽江", + "101290701": "普洱", + "101290801": "临沧", + "101290901": "楚雄", + "101291001": "红河", + "101291101": "文山", + "101291201": "西双版纳", + "101291301": "大理", + "101291401": "德宏", + "101291501": "怒江", + "101291601": "迪庆", + + # 广西省 + "101300101": "南宁", + "101300201": "柳州", + "101300301": "桂林", + "101300401": "梧州", + "101300501": "北海", + "101300601": "防城港", + "101300701": "钦州", + "101300801": "贵港", + "101300901": "玉林", + "101301001": "百色", + "101301101": "贺州", + "101301201": "河池", + "101301301": "来宾", + "101301401": "崇左", + + # 贵州省 + "101260101": "贵阳", + "101260201": "六盘水", + "101260301": "遵义", + "101260401": "安顺", + "101260501": "毕节", + "101260601": "铜仁", + "101260901": "黔西南", + "101261001": "黔东南", + "101261101": "黔南", + + # 河北省 + "101090101": "石家庄", + "101090201": "唐山", + "101090301": "秦皇岛", + "101090401": "邯郸", + "101090501": "邢台", + "101090601": "保定", + "101090701": "张家口", + "101090801": "承德", + "101090901": "沧州", + "101091001": "廊坊", + "101091101": "衡水", + + # 山西省 + "101100101": "太原", + "101100201": "大同", + "101100301": "阳泉", + "101100401": "长治", + "101100501": "晋城", + "101100601": "朔州", + "101100701": "晋中", + "101100801": "运城", + "101100901": "忻州", + "101101001": "临汾", + "101101101": "吕梁", + + # 黑龙江省 + "101050101": "哈尔滨", + "101050201": "齐齐哈尔", + "101050301": "鸡西", + "101050401": "鹤岗", + "101050501": "双鸭山", + "101050601": "大庆", + "101050701": "伊春", + "101050801": "佳木斯", + "101050901": "七台河", + "101051001": "牡丹江", + "101051101": "黑河", + "101051201": "绥化", + "101051301": "大兴安岭", + + # 吉林省 + "101060101": "长春", + "101060201": "吉林", + "101060301": "四平", + "101060401": "辽源", + "101060501": "通化", + "101060601": "白山", + "101060701": "松原", + "101060801": "白城", + "101060901": "延边", + + # 内蒙古 + "101080101": "呼和浩特", + "101080201": "包头", + "101080301": "乌海", + "101080401": "赤峰", + "101080501": "通辽", + "101080601": "鄂尔多斯", + "101080701": "呼伦贝尔", + "101080801": "巴彦淖尔", + "101080901": "乌兰察布", + "101081001": "兴安", + "101081101": "锡林郭勒", + "101081201": "阿拉善", + + # 新疆 + "101130101": "乌鲁木齐", + "101130201": "克拉玛依", + "101130301": "吐鲁番", + "101130401": "哈密", + "101130501": "昌吉", + "101130601": "博尔塔拉", + "101130701": "巴音郭楞", + "101130801": "阿克苏", + "101130901": "克孜勒苏", + "101131001": "喀什", + "101131101": "和田", + "101131201": "伊犁", + "101131301": "塔城", + "101131401": "阿勒泰", + "101131501": "石河子", + "101131601": "阿拉尔", + "101131701": "图木舒克", + "101131801": "五家渠", + "101131901": "北屯", + "101132001": "铁门关", + "101132101": "双河", + "101132201": "可克达拉", + "101132301": "昆玉", + "101132401": "胡杨河", + + # 西藏 + "101140101": "拉萨", + "101140201": "日喀则", + "101140301": "昌都", + "101140401": "林芝", + "101140501": "山南", + "101140601": "那曲", + "101140701": "阿里", + + # 青海省 + "101150101": "西宁", + "101150201": "海东", + "101150301": "海北", + "101150401": "黄南", + "101150501": "海南", + "101150601": "果洛", + "101150701": "玉树", + "101150801": "海西", + + # 甘肃省 + "101160101": "兰州", + "101160201": "嘉峪关", + "101160301": "金昌", + "101160401": "白银", + "101160501": "天水", + "101160601": "武威", + "101160701": "张掖", + "101160801": "平凉", + "101160901": "酒泉", + "101161001": "庆阳", + "101161101": "定西", + "101161201": "陇南", + "101161301": "临夏", + "101161401": "甘南", + + # 宁夏 + "101170101": "银川", + "101170201": "石嘴山", + "101170301": "吴忠", + "101170401": "固原", + "101170501": "中卫", + + # 海南省 + "101310101": "海口", + "101310201": "三亚", + "101310301": "三沙", + "101310401": "儋州", + "101310501": "五指山", + "101310601": "琼海", + "101310701": "文昌", + "101310801": "万宁", + "101310901": "东方", + "101311001": "定安", + "101311101": "屯昌", + "101311201": "澄迈", + "101311301": "临高", + "101311401": "白沙", + "101311501": "昌江", + "101311601": "乐东", + "101311701": "陵水", + "101311801": "保亭", + "101311901": "琼中", +} + +# 城市名称到编码的映射(用于模糊搜索) +CITY_NAME_TO_CODE = {v: k for k, v in CITY_CODES.items()} + + +def find_city_code(city_name: str) -> str: + """ + 根据城市名称查找编码 + + Args: + city_name: 城市名称 + + Returns: + 城市编码,未找到返回 None + """ + # 精确匹配 + if city_name in CITY_NAME_TO_CODE: + return CITY_NAME_TO_CODE[city_name] + + # 模糊匹配 + for name, code in CITY_NAME_TO_CODE.items(): + if city_name in name or name in city_name: + return code + + return None + + +def get_city_name(code: str) -> str: + """ + 根据编码获取城市名称 + + Args: + code: 城市编码 + + Returns: + 城市名称,未找到返回 None + """ + return CITY_CODES.get(code) + + +if __name__ == "__main__": + # 测试代码 + print("测试城市编码查询:") + test_cities = ["杭州", "北京", "上海", "深圳", "成都"] + for city in test_cities: + code = find_city_code(city) + print(f"{city}: {code}") diff --git a/skills/weather/scripts/weather/query.py b/skills/weather/scripts/weather/query.py new file mode 100644 index 0000000..0e76baa --- /dev/null +++ b/skills/weather/scripts/weather/query.py @@ -0,0 +1,399 @@ +# -*- coding: utf-8 -*- +""" +中国天气网天气查询模块 +数据来源:www.weather.com.cn +""" + +import json +import re +from typing import Optional, Dict, Any, List +from dataclasses import dataclass +from datetime import datetime + +import httpx + +from .city_codes import find_city_code, get_city_name, CITY_CODES + + +@dataclass +class WeatherInfo: + """天气信息数据类""" + city: str # 城市名称 + date: str # 日期 + weather: str # 天气状况 + temperature: str # 温度范围 + wind: str # 风向风力 + humidity: str = "" # 湿度 + update_time: str = "" # 更新时间 + + +class WeatherQuery: + """天气查询类""" + + BASE_URL = "https://www.weather.com.cn" + 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", + "Referer": "https://www.weather.com.cn/", + } + + def __init__(self, timeout: int = 10): + """ + 初始化天气查询 + + Args: + timeout: 请求超时时间(秒) + """ + self.timeout = timeout + self.client = httpx.Client(headers=self.HEADERS, timeout=timeout, follow_redirects=True) + + def __del__(self): + """关闭客户端""" + if hasattr(self, 'client'): + self.client.close() + + def _parse_weather_html(self, html: str, city: str) -> List[WeatherInfo]: + """ + 解析天气HTML页面 + + Args: + html: HTML内容 + city: 城市名称 + + Returns: + 天气信息列表 + """ + weather_list = [] + + # 提取更新时间 + update_time_match = re.search(r'(\d{2}:\d{2})\s*更新', html) + update_time = update_time_match.group(1) if update_time_match else "" + + # 方法1: 尝试从HTML中直接提取天气预报数据 + # 查找所有包含天气信息的li标签 + # HTML结构示例: + #
晴
...
]*>([^<]{1,20})
', weather_section) + weather = weather_match.group(1).strip() if weather_match else "" + + # 提取温度(格式如: "21"/8℃ 或 21/8℃) + temp_pattern = r'>(\d{1,3})<.*?/.*?>(\d{1,3})℃?' + temp_match = re.search(temp_pattern, weather_section) + if temp_match: + temp_high = temp_match.group(1) + temp_low = temp_match.group(2) + temperature = f"{temp_high}/{temp_low}℃" + else: + # 尝试另一种温度格式 + temp_match2 = re.search(r'(\d{1,3})[/-](\d{1,3})', weather_section) + if temp_match2: + temperature = f"{temp_match2.group(1)}/{temp_match2.group(2)}℃" + else: + temperature = "" + + # 提取风向风力 + # 提取风向(如:东北风、东风等) + wind_dir_match = re.search(r'(东风|南风|西风|北风|东北风|东南风|西北风|西南风)', weather_section) + wind_dir = wind_dir_match.group(1) if wind_dir_match else "" + # 提取风力等级 + wind_level_match = re.search(r'<(\d级|无持续风向|微风|和风)>', weather_section) + wind_level = wind_level_match.group(1) if wind_level_match else "" + if not wind_level: + wind_level_match = re.search(r'(\d级|<\d级)', weather_section) + wind_level = wind_level_match.group(1) if wind_level_match else "" + + wind = f"{wind_dir} {wind_level}".strip() if wind_dir or wind_level else "" + + weather_list.append(WeatherInfo( + city=city, + date=date_str, + weather=weather, + temperature=temperature, + wind=wind, + update_time=update_time + )) + + # 方法2: 如果上述方法失败,尝试查找所有li标签的内容 + if not weather_list: + li_pattern = r']*>([^<]+)
.*?(\d+).*?/.*?(\d+)' + matches = re.findall(li_pattern, html, re.DOTALL) + for match in matches: + date, weather, temp_high, temp_low = match + weather_list.append(WeatherInfo( + city=city, + date=date.strip(), + weather=weather.strip(), + temperature=f"{temp_high}/{temp_low}℃", + wind="", + update_time=update_time + )) + + return weather_list + + def _get_weather_api(self, city_code: str) -> Optional[Dict[str, Any]]: + """ + 通过API获取天气数据 + + Args: + city_code: 城市编码 + + Returns: + 天气数据字典 + """ + api_url = f"{self.BASE_URL}/data/sk/2d/{city_code}.html" + try: + response = self.client.get(api_url) + if response.status_code == 200: + data = response.text + # 移除可能的JSONP回调 + data = re.sub(r'^.*?\(', '', data) + data = re.sub(r'\);*$', '', data) + return json.loads(data) + except Exception as e: + pass + return None + + def query_by_code(self, city_code: str, days: int = 7) -> Optional[List[WeatherInfo]]: + """ + 根据城市编码查询天气 + + Args: + city_code: 城市编码 + days: 查询天数(1, 7, 15, 40) + + Returns: + 天气信息列表 + """ + city_name = get_city_name(city_code) or "未知" + + # 根据天数选择不同的URL + if days == 1: + url = f"{self.BASE_URL}/weather1d/{city_code}.shtml" + elif days == 15: + url = f"{self.BASE_URL}/weather15d/{city_code}.shtml" + elif days == 40: + url = f"{self.BASE_URL}/weather40d/{city_code}.shtml" + else: # 默认7天 + url = f"{self.BASE_URL}/weather/{city_code}.shtml" + + try: + response = self.client.get(url) + if response.status_code == 200: + weather_list = self._parse_weather_html(response.text, city_name) + return weather_list + except Exception as e: + pass + + return None + + def query(self, city: str, days: int = 7) -> Optional[List[WeatherInfo]]: + """ + 根据城市名称查询天气 + + Args: + city: 城市名称 + days: 查询天数(1, 7, 15, 40) + + Returns: + 天气信息列表 + """ + city_code = find_city_code(city) + if not city_code: + return None + + return self.query_by_code(city_code, days) + + def query_current(self, city: str) -> Optional[Dict[str, Any]]: + """ + 查询当前天气 + + Args: + city: 城市名称 + + Returns: + 当前天气信息 + """ + city_code = find_city_code(city) + if not city_code: + return None + + # 获取实时天气API + api_url = f"{self.BASE_URL}/data/sk/2d/{city_code}.html" + try: + response = self.client.get(api_url) + if response.status_code == 200: + data = response.text + # 移除可能的JSONP回调 + data = re.sub(r'^.*?\(', '', data) + data = re.sub(r'\);*$', '', data) + weather_data = json.loads(data) + + # 解析返回的数据 + if weather_data.get("weatherinfo"): + info = weather_data["weatherinfo"] + return { + "city": info.get("city", ""), + "temp": f"{info.get('temp', '')}°C", + "weather": info.get("weather", ""), + "wind": f"{info.get('wd', '')}{info.get('ws', '')}", + "humidity": f"{info.get('sd', '')}", + "time": info.get("time", ""), + } + except Exception as e: + pass + + return None + + def format_weather(self, weather_info: List[WeatherInfo], days: int = 7) -> str: + """ + 格式化天气信息为可读文本 + + Args: + weather_info: 天气信息列表 + days: 显示天数 + + Returns: + 格式化的天气文本 + """ + if not weather_info: + return "未能获取天气信息" + + result = [f"📍 {weather_info[0].city} 天气预报"] + result.append("=" * 30) + + for i, info in enumerate(weather_info[:days]): + if i == 0: + result.append(f"\n📅 {info.date} (今天)") + else: + result.append(f"\n📅 {info.date}") + + result.append(f" 🌤️ {info.weather}") + result.append(f" 🌡️ {info.temperature}") + if info.wind: + result.append(f" 💨 {info.wind}") + + if weather_info[0].update_time: + result.append(f"\n⏰ 更新时间: {weather_info[0].update_time}") + + return "\n".join(result) + + def search_city(self, keyword: str) -> List[Dict[str, str]]: + """ + 搜索城市 + + Args: + keyword: 搜索关键词 + + Returns: + 匹配的城市列表 + """ + results = [] + keyword_lower = keyword.lower() + + for code, name in CITY_CODES.items(): + if keyword_lower in name.lower(): + results.append({"code": code, "name": name}) + + # 按匹配度排序 + results.sort(key=lambda x: ( + not x["name"].startswith(keyword), + len(x["name"]) + )) + + return results[:20] # 返回前20个结果 + + +# 便捷函数 +def query_weather(city: str, days: int = 7) -> Optional[str]: + """ + 查询天气(便捷函数) + + Args: + city: 城市名称 + days: 查询天数 + + Returns: + 格式化的天气文本 + """ + query = WeatherQuery() + weather_info = query.query(city, days) + if weather_info: + return query.format_weather(weather_info, days) + return None + + +def query_current_weather(city: str) -> Optional[str]: + """ + 查询当前天气(便捷函数) + + Args: + city: 城市名称 + + Returns: + 格式化的当前天气文本 + """ + query = WeatherQuery() + current = query.query_current(city) + if current: + return f"📍 {current['city']} 当前天气\n" \ + f"🌡️ 温度: {current['temp']}\n" \ + f"🌤️ 天气: {current['weather']}\n" \ + f"💨 风向风力: {current['wind']}\n" \ + f"💧 湿度: {current['humidity']}\n" \ + f"⏰ 更新时间: {current['time']}" + return None + + +def search_city(keyword: str) -> List[Dict[str, str]]: + """ + 搜索城市(便捷函数) + + Args: + keyword: 搜索关键词 + + Returns: + 匹配的城市列表 + """ + query = WeatherQuery() + return query.search_city(keyword) + + +if __name__ == "__main__": + # 测试代码 + print("测试天气查询:") + + # 测试当前天气 + print("\n=== 当前天气 ===") + current = query_current_weather("杭州") + print(current) + + # 测试7天天气预报 + print("\n=== 7天天气预报 ===") + weather = query_weather("杭州", 7) + print(weather) + + # 测试城市搜索 + print("\n=== 城市搜索 ===") + cities = search_city("州") + for city in cities[:5]: + print(f" {city['name']}: {city['code']}") diff --git a/skills/weather/scripts/weather_query.py b/skills/weather/scripts/weather_query.py new file mode 100644 index 0000000..98adfd6 --- /dev/null +++ b/skills/weather/scripts/weather_query.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +中国天气网天气查询命令行工具 +数据来源:www.weather.com.cn +""" + +import argparse +import json +import sys +from weather import query_weather, query_current_weather, search_city, find_city_code + + +def cmd_query(args): + """查询天气预报""" + city_code = find_city_code(args.city) + if not city_code: + print(f"错误:未找到城市 \"{args.city}\",请使用 search 命令搜索城市") + return 1 + + if args.current: + # 查询当前天气 + result = query_current_weather(args.city) + if result: + print(result) + return 0 + else: + print(f"错误:无法获取 {args.city} 的当前天气") + return 1 + else: + # 查询天气预报 + days = args.days or 7 + result = query_weather(args.city, days) + if result: + print(result) + return 0 + else: + print(f"错误:无法获取 {args.city} 的天气预报") + return 1 + + +def cmd_search(args): + """搜索城市""" + cities = search_city(args.keyword) + if not cities: + print(f"未找到包含 \"{args.keyword}\" 的城市") + return 1 + + print(f"找到 {len(cities)} 个匹配的城市:") + print("-" * 40) + for city in cities[:args.limit]: + print(f" {city['name']}: {city['code']}") + return 0 + + +def cmd_json(args): + """输出JSON格式的天气数据""" + city_code = find_city_code(args.city) + if not city_code: + print(f"错误:未找到城市 \"{args.city}\"") + return 1 + + from weather import WeatherQuery + query = WeatherQuery() + weather_info = query.query(args.city, args.days or 7) + + if weather_info: + data = { + "city": weather_info[0].city, + "update_time": weather_info[0].update_time, + "forecast": [ + { + "date": info.date, + "weather": info.weather, + "temperature": info.temperature, + "wind": info.wind, + } + for info in weather_info + ] + } + print(json.dumps(data, ensure_ascii=False, indent=2)) + return 0 + else: + print(f"错误:无法获取 {args.city} 的天气数据") + return 1 + + +def main(): + parser = argparse.ArgumentParser( + description="中国天气网天气查询工具", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + %(prog)s query 杭州 # 查询杭州7天天气预报 + %(prog)s query 北京 --days 3 # 查询北京3天天气预报 + %(prog)s query 深圳 --current # 查询深圳当前天气 + %(prog)s search 州 # 搜索带"州"字的城市 + %(prog)s json 杭州 --days 3 # 输出JSON格式的天气数据 + """ + ) + + subparsers = parser.add_subparsers(dest="command", help="子命令") + + # query 命令 + query_parser = subparsers.add_parser("query", help="查询天气预报") + query_parser.add_argument("city", help="城市名称,如:杭州、北京、上海") + query_parser.add_argument("--days", type=int, choices=[1, 3, 7, 15, 40], + help="查询天数 (1, 3, 7, 15, 40),默认7天") + query_parser.add_argument("--current", "-c", action="store_true", + help="查询当前天气而非预报") + query_parser.set_defaults(func=cmd_query) + + # search 命令 + search_parser = subparsers.add_parser("search", help="搜索城市") + search_parser.add_argument("keyword", help="搜索关键词") + search_parser.add_argument("--limit", type=int, default=20, + help="返回结果数量限制,默认20") + search_parser.set_defaults(func=cmd_search) + + # json 命令 + json_parser = subparsers.add_parser("json", help="输出JSON格式的天气数据") + json_parser.add_argument("city", help="城市名称") + json_parser.add_argument("--days", type=int, choices=[1, 3, 7, 15, 40], + help="查询天数,默认7天") + json_parser.set_defaults(func=cmd_json) + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return 1 + + return args.func(args) + + +if __name__ == "__main__": + sys.exit(main())