技术博客
天气预报国际版:多城市天气批量管理(无批量接口下的循环与并发控制)

天气预报国际版:多城市天气批量管理(无批量接口下的循环与并发控制)

作者: 万维易源
2026-09-03
天气预报国际版多城市管理限速并发令牌桶
# 天气预报国际版:多城市天气批量管理(无批量接口下的循环与并发控制) > 接口:天气预报国际版(apiCode=3540)全部接入点 · 免费接口 · POST/GET · JSON · 适用人群:后端/中高级开发者 · 阅读时间:约 7 分钟 ## 核心要点 - 天气预报国际版**没有提供批量查询接入点**(文档接入点仅"当前天气 / 24 小时预报 / 14 天预报"三个,均为单城市粒度)——多城市需求需要客户端自行循环调用。 - 本文给出生产级的多城市拉取方案:限速(令牌桶)+ 有界并发 + 指数退避重试,把"几百个城市定时刷新"跑稳。 - 城市清单务必落库管理(城市名或坐标),每个城市固定一种定位方式,避免同一城市两种定位来回漂移。 ## Why 旅游平台要展示 200 个目的地天气,IoT 面板要轮询全国门店所在城市,资讯早报要汇总 30 个城市天气——多城市是天气接口最常见的规模化场景。 接口本身按单城市设计,这反而是好事:返回结构简单、单请求轻量。真正的工程问题在客户端侧——怎么在不触发限流、不打垮自己的前提下有序地循环调用。本文把这套模式一次讲全。 ## What | 项目 | 说明 | |------|------| | 批量能力 | 无批量接入点,多城市 = 客户端循环单城市调用(文档明确接入点只有 3 个) | | 单次请求 | 一个城市 + 一个接入点,成功 `showapi_fee_num=1`,失败为 0(实测) | | 失败行为 | 定位失败 `ret_code=-1`「经纬度不能为空」(实测),不扣次 | | 配套能力 | 无订阅/回调推送;数据获取为同步拉取模式 | | 建议 | 循环 + 限速 + 重试 + 缓存(见[缓存策略](https://www.showapi.com/guides/global-weather-cache-cost-3540)) | ## How ### 1. 城市清单落库 用一张表管理城市与定位方式,避免散落在代码里: ```python CITIES = [ # (显示名, 定位参数dict) ("北京", {"name": "北京"}), ("伦敦", {"name": "London"}), ("曼谷", {"name": "bangkok"}), ("东京", {"lon": "139.69", "lat": "35.69"}), ] ``` ### 2. 限速 + 并发 + 重试的多城市拉取(Python) ```python # pip install requests import threading import time import requests from concurrent.futures import ThreadPoolExecutor, as_completed APPKEY = "YOUR_APPKEY" class TokenBucket: """令牌桶限速:rate 个/秒。""" def __init__(self, rate: float): self.rate, self.capacity = rate, rate self.tokens, self.ts = rate, time.monotonic() self.lock = threading.Lock() def take(self): with self.lock: now = time.monotonic() self.tokens = min(self.capacity, self.tokens + (now - self.ts) * self.rate) self.ts = now if self.tokens < 1: time.sleep((1 - self.tokens) / self.rate) # 等待令牌 self.tokens = 0 else: self.tokens -= 1 bucket = TokenBucket(rate=5) # 全局 5 QPS,按档位自行调整 def fetch_one(endpoint: str, loc: dict, retries: int = 3) -> dict: params = {"appKey": APPKEY, **loc} for attempt in range(retries): bucket.take() # 先拿令牌再发请求 try: resp = requests.post(f"https://route.showapi.com/{endpoint}", params=params, timeout=10) body = resp.json()["showapi_res_body"] if body.get("ret_code") == 0: return body # 业务失败(如定位解析失败)重试无意义,直接抛出 raise ValueError(f"业务失败: {body.get('remark')} loc={loc}") except (requests.Timeout, requests.ConnectionError) as e: if attempt == retries - 1: raise time.sleep(2 ** attempt) # 指数退避:1s/2s/4s def fetch_many(endpoint: str, cities: list, workers: int = 4) -> dict: """有界并发拉取多城市,返回 {城市名: body 或 错误信息}。""" result = {} with ThreadPoolExecutor(max_workers=workers) as pool: futs = {pool.submit(fetch_one, endpoint, loc): label for label, loc in cities} for fut in as_completed(futs): label = futs[fut] try: result[label] = fut.result() except Exception as e: result[label] = {"error": str(e)} return result data = fetch_many("3540-1", CITIES) for label, body in data.items(): if "error" in body: print(label, "FAIL:", body["error"]) else: now = body["now"] print(f'{label}: {now["temperature"]}℃ {now["weather"]}') ``` cURL(单城市回源对照): ```bash curl -X POST "https://route.showapi.com/3540-1?appKey=YOUR_APPKEY&name=London" ``` Node.js(p-limit 有界并发要点): ```js const pLimit = (await import("p-limit")).default; const limit = pLimit(4); // 有界并发 const delay = (ms) => new Promise(r => setTimeout(r, ms)); async function fetchOne(name, attempt = 0) { const res = await fetch( `https://route.showapi.com/3540-1?appKey=${process.env.APPKEY}&name=${encodeURIComponent(name)}`, { method: "POST" } ); const body = (await res.json()).showapi_res_body; if (body.ret_code === 0) return body; if (attempt < 2) { await delay(2 ** attempt * 1000); return fetchOne(name, attempt + 1); } throw new Error(`${body.remark}`); } const bodies = await Promise.all(cities.map(c => limit(() => fetchOne(c)))); ``` ### 3. 定时刷新编排 推荐结构:**调度器(cron/定时任务)→ 限速循环拉取 → 写缓存 → 读请求全部走缓存**。回源频率由调度器决定,用户请求永不直接打接口。 ## 返回示例与解析 多城市拉取的单份返回就是各接入点的标准结构(当前天气示例): ```json { "ret_code": 0, "cityInfo": { "city": "伦敦", "city_en": "London", "time_zone": "Europe/London" }, "now": { "temperature": 18.9, "weather": "阴天", "rain_prop": 24 } } ``` 编排层要做的事是把 N 份这样的结构按城市落位:成功写缓存、失败记录城市与原因(注意记录你传入的定位参数——接口侧对无效城市名与缺参数返回同一错误,见[定位参数](https://www.showapi.com/guides/global-weather-location-params-3540))。 ## 进阶与边界 - **速率上限**:文档未给出明确的 QPS 数值(免费档位限制以[官方档位说明](https://www.showapi.com/free-api)为准)。上表 `rate=5` 是保守起点,结合档位说明与实际响应观察调整。 - **定位方式统一**:同一城市固定用 `name` 或固定用坐标,不要混用,否则解析结果可能不一致(实测同名解析存在出入的案例,见[14 天预报实战](https://www.showapi.com/guides/global-weather-14day-forecast-3540)的对账说明)。 - **失败的治理**:把 `ret_code != 0` 的城市单独进"问题清单"人工复核,不要自动重试业务失败(重试网络失败即可)。 - **别做无谓的并发**:并发数超过限速能力只会堆等待,`workers × 单请求耗时` 与令牌桶速率匹配即可。 ## FAQ **Q1:接口支持一次传多个城市名吗?** 不支持。文档接入点为单城市粒度(`name` 或 `lon`/`lat` 单值参数),批量需求用客户端循环实现。 **Q2:几百个城市轮询会不会被限流?** 免费档位设有防滥用限制(具体数值文档未给)。用令牌桶限速 + 缓存兜底,把回源 QPS 控制在档位允许范围内即可稳定运行。 **Q3:有没有订阅推送/回调,让服务端主动推天气更新?** 文档未提供该能力,三个接入点均为同步拉取模式。数据更新靠你的调度器定时拉取。 **Q4:某个城市一直返回"经纬度不能为空"怎么办?** 这是定位解析失败(实测与不传参数同错误)。检查该城市的定位参数:中文生僻地名换英文或坐标,坐标换城市名,交叉验证。 **Q5:多城市结果怎么保证是"同一时刻"的快照?** 同步拉取本身有时间差。对"同一时刻"要求高的场景,记录每份返回的 `cityInfo.localtime` 做对齐展示,而不是假设全部同时返回。 ## 下一步阅读 - [天气预报国际版:免费档位下的缓存策略设计(用 Redis 省调用量)](https://www.showapi.com/guides/global-weather-cache-cost-3540) - [天气预报国际版:定位参数怎么传(城市名 name 与经纬度 lon/lat 的选择)](https://www.showapi.com/guides/global-weather-location-params-3540) - [天气预报国际版:通过 MCP 在 AI 客户端中直接查天气](https://www.showapi.com/guides/global-weather-mcp-integration-3540) - **本系列共 12 篇**:查看[天气预报国际版指南总目录](https://www.showapi.com/guides/global-weather-guides-3540)