This commit is contained in:
朱潮 2026-04-04 08:01:21 +08:00
parent f3165944d2
commit 2cc8b893f7
7 changed files with 0 additions and 1386 deletions

View File

@ -1,13 +0,0 @@
{
"name": "weather",
"description": "提供中国天气预报查询功能支持全国300+城市的当前天气和未来1/3/7/15/40天天气预报查询。",
"mcpServers": {
"weather": {
"transport": "stdio",
"command": "python",
"args": [
"./skills/weather/mcp.py"
]
}
}
}

View File

@ -1,149 +0,0 @@
---
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天预报查询

View File

@ -1,123 +0,0 @@
#!/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())

View File

@ -1,40 +0,0 @@
# -*- 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"

View File

@ -1,525 +0,0 @@
# -*- 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}")

View File

@ -1,399 +0,0 @@
# -*- 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结构示例:
# <li><h1>15日今天</h1><p>晴</p><p><generic>"21"</generic>/<generic>8℃</generic></p><p>...</p></li>
# 首先尝试查找h1标签的日期
date_pattern = r'<h1[^>]*>(\d+日|[^<]*?(今天)|[^<]*?(明天)|[^<]*?(后天)|周[一二三四五六七])</h1>'
date_matches = list(re.finditer(date_pattern, html))
for date_match in date_matches:
# 获取日期
date_str = date_match.group(1)
# 从日期位置开始,向后查找天气信息
start_pos = date_match.end()
# 找到下一个h1或li结束的位置
next_h1 = re.search(r'<h1|</li>', html[start_pos:start_pos + 500])
if next_h1:
end_pos = start_pos + next_h1.start()
else:
end_pos = start_pos + 500
weather_section = html[start_pos:end_pos]
# 提取天气状况通常在p标签中
weather_match = re.search(r'<p[^>]*>([^<]{1,20})</p>', 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'<li[^>]*>.*?<h1>([^<]+)</h1>.*?<p[^>]*>([^<]+)</p>.*?(\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']}")

View File

@ -1,137 +0,0 @@
#!/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())