天气预报国际版:多城市天气批量管理(无批量接口下的循环与并发控制)
# 天气预报国际版:多城市天气批量管理(无批量接口下的循环与并发控制)
> 接口:天气预报国际版(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)