weather.py
4.9 KB| 1 | #!/usr/bin/env python3 |
| 2 | # 天气查询(Open-Meteo 免费 API,无需 Key) |
| 3 | import argparse, json, sys, urllib.parse, urllib.request |
| 4 | |
| 5 | GEO_URL = 'https://geocoding-api.open-meteo.com/v1/search' |
| 6 | WX_URL = 'https://api.open-meteo.com/v1/forecast' |
| 7 | |
| 8 | # WMO weather code -> 中文 / 英文 |
| 9 | WMO = { |
| 10 | 0: ('晴', 'Clear sky'), |
| 11 | 1: ('基本晴', 'Mainly clear'), |
| 12 | 2: ('多云', 'Partly cloudy'), |
| 13 | 3: ('阴', 'Overcast'), |
| 14 | 45: ('雾', 'Fog'), |
| 15 | 48: ('冻雾', 'Depositing rime fog'), |
| 16 | 51: ('毛毛雨', 'Light drizzle'), |
| 17 | 53: ('小到中雨', 'Drizzle'), |
| 18 | 55: ('雨', 'Dense drizzle'), |
| 19 | 56: ('冻毛毛雨', 'Freezing drizzle'), |
| 20 | 57: ('强冻毛毛雨', 'Dense freezing drizzle'), |
| 21 | 61: ('小雨', 'Slight rain'), |
| 22 | 63: ('中雨', 'Moderate rain'), |
| 23 | 65: ('大雨', 'Heavy rain'), |
| 24 | 66: ('冻雨', 'Freezing rain'), |
| 25 | 67: ('强冻雨', 'Heavy freezing rain'), |
| 26 | 71: ('小雪', 'Slight snow'), |
| 27 | 73: ('中雪', 'Moderate snow'), |
| 28 | 75: ('大雪', 'Heavy snow'), |
| 29 | 77: ('雪粒', 'Snow grains'), |
| 30 | 80: ('阵雨', 'Slight showers'), |
| 31 | 81: ('阵雨', 'Moderate showers'), |
| 32 | 82: ('强阵雨', 'Violent showers'), |
| 33 | 85: ('阵雪', 'Slight snow showers'), |
| 34 | 86: ('强阵雪', 'Heavy snow showers'), |
| 35 | 95: ('雷雨', 'Thunderstorm'), |
| 36 | 96: ('雷雨伴冰雹', 'Thunderstorm with hail'), |
| 37 | 99: ('强雷雨伴冰雹', 'Thunderstorm with heavy hail'), |
| 38 | } |
| 39 | |
| 40 | |
| 41 | def fetch(url): |
| 42 | req = urllib.request.Request(url, headers={'User-Agent': 'weather-skill/1.0'}) |
| 43 | with urllib.request.urlopen(req, timeout=15) as r: |
| 44 | return json.loads(r.read().decode()) |
| 45 | |
| 46 | |
| 47 | def geocode(name): |
| 48 | q = urllib.parse.quote(name) |
| 49 | data = fetch(f'{GEO_URL}?name={q}&count=1&language=zh&format=json') |
| 50 | results = data.get('results') or [] |
| 51 | if not results: |
| 52 | return None |
| 53 | r = results[0] |
| 54 | return r.get('latitude'), r.get('longitude'), r.get('name'), r.get('country'), r.get('admin1') |
| 55 | |
| 56 | |
| 57 | def weather(lat, lon, days, lang): |
| 58 | params = { |
| 59 | 'latitude': lat, 'longitude': lon, |
| 60 | 'current': 'temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,wind_direction_10m', |
| 61 | 'daily': 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max', |
| 62 | 'timezone': 'auto', 'forecast_days': days, |
| 63 | } |
| 64 | q = urllib.parse.urlencode(params) |
| 65 | data = fetch(f'{WX_URL}?{q}') |
| 66 | cur = data.get('current', {}) |
| 67 | daily = data.get('daily', {}) |
| 68 | lines = [] |
| 69 | # 当前 |
| 70 | code = cur.get('weather_code', -1) |
| 71 | if lang == 'zh': |
| 72 | desc = WMO.get(code, ('未知', 'Unknown'))[0] |
| 73 | lines.append(f"当前:{desc} {cur.get('temperature_2m')}°C(体感 {cur.get('apparent_temperature')}°C) " |
| 74 | f"湿度 {cur.get('relative_humidity_2m')}% 风速 {cur.get('wind_speed_10m')} km/h") |
| 75 | else: |
| 76 | desc = WMO.get(code, ('未知', 'Unknown'))[1] |
| 77 | lines.append(f"Now: {desc} {cur.get('temperature_2m')}°C (feels like {cur.get('apparent_temperature')}°C) " |
| 78 | f"humidity {cur.get('relative_humidity_2m')}% wind {cur.get('wind_speed_10m')} km/h") |
| 79 | # 预报 |
| 80 | times = daily.get('time', []) |
| 81 | for i, t in enumerate(times): |
| 82 | d = daily.get('weather_code', [])[i] if daily.get('weather_code') else -1 |
| 83 | dmax = daily.get('temperature_2m_max', [])[i] if daily.get('temperature_2m_max') else '?' |
| 84 | dmin = daily.get('temperature_2m_min', [])[i] if daily.get('temperature_2m_min') else '?' |
| 85 | pop = daily.get('precipitation_probability_max', [])[i] if daily.get('precipitation_probability_max') else 0 |
| 86 | if lang == 'zh': |
| 87 | desc = WMO.get(d, ('未知', 'Unknown'))[0] |
| 88 | tag = '今天' if i == 0 else t[5:] |
| 89 | lines.append(f"{tag}:{desc} {dmin}~{dmax}°C 降水概率 {pop}%") |
| 90 | else: |
| 91 | desc = WMO.get(d, ('未知', 'Unknown'))[1] |
| 92 | lines.append(f"{t}: {desc} {dmin}~{dmax}°C precip {pop}%") |
| 93 | return '\n'.join(lines) |
| 94 | |
| 95 | |
| 96 | def main(): |
| 97 | p = argparse.ArgumentParser(description='天气查询') |
| 98 | p.add_argument('place', nargs='?', help='城市名(中英文均可)') |
| 99 | p.add_argument('--lat', type=float) |
| 100 | p.add_argument('--lon', type=float) |
| 101 | p.add_argument('--days', type=int, default=1) |
| 102 | p.add_argument('--lang', choices=['zh', 'en'], default='zh') |
| 103 | a = p.parse_args() |
| 104 | if a.place: |
| 105 | g = geocode(a.place) |
| 106 | if not g: |
| 107 | print(f'找不到地点: {a.place}') |
| 108 | sys.exit(1) |
| 109 | lat, lon, name, country, admin1 = g |
| 110 | label = f'{name}({admin1}, {country})' if a.lang == 'zh' else f'{name} ({admin1}, {country})' |
| 111 | elif a.lat is not None and a.lon is not None: |
| 112 | lat, lon, label = a.lat, a.lon, f'{a.lat},{a.lon}' |
| 113 | else: |
| 114 | p.error('需要城市名或 --lat/--lon') |
| 115 | return |
| 116 | try: |
| 117 | out = weather(lat, lon, a.days, a.lang) |
| 118 | print(f'{label}\n{out}') |
| 119 | except Exception as e: |
| 120 | print(f'查询失败: {e}') |
| 121 | sys.exit(1) |
| 122 | |
| 123 | |
| 124 | if __name__ == '__main__': |
| 125 | main() |
文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。

