API 接入与调用教程:Python、PHP、JS 实战
API.PLUS 的三个示例接口共用一套 apikey,Python、PHP、JavaScript 的接入差异集中在超时单位、连接复用与重试判定,不在请求本身的拼装。
- 先跑通 GET 与随机动漫壁纸接口,再处理 POST 与幂等
- requests 用元组超时,PHP curl 用毫秒,fetch 用 AbortSignal
- 只对 429 和 5xx 退避重试,4xx 重试只会放大失败
结论:API.PLUS 的接口用一套 apikey 就能调通,Python、PHP、JavaScript 的差别不在请求拼装,而在超时单位、连接复用和重试判定。跑通三个端点大约十分钟,把重试写对要半天。
上周我把一个采集脚本从 Python 换到 PHP 跑,接口没变,失败率从 0.3% 涨到 4%。查下来是 curl 没设 CURLOPT_CONNECTTIMEOUT_MS,建连阈值太松,遇到网络抖动整批卡住。这类问题跟接口文档没关系,全在客户端这一侧。下面按我实际踩坑的顺序写。
三个端点先跑通:GET、随机动漫壁纸与 POST 的差别在哪?#
API.PLUS(沐小悠API,https://www.api.plus)的文档入口分得比较细,GET 示例接口、随机动漫壁纸接口、POST 示例接口 各一页。调用形态不一样,先看对照表。
| 端点 | 方法 | 关键参数 | 返回形态 | 典型用途 |
|---|---|---|---|---|
| https://www.api.plus/api/get | GET | apikey、ip | JSON 对象 | 查询类接口,可缓存、可重试 |
| https://www.api.plus/api/dongman | GET | type=json | JSON,含图片直链 | 内容展示,适合 CDN(Content Delivery Network,内容分发网络) 前置 |
| POST 示例端点(见 /doc/post) | POST | apikey、请求体字段 | JSON | 写请求,需考虑幂等 |
curl 是最快的验证手段。-w 把 DNS、建连、总耗时打出来,比只看状态码有用得多。
示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。
curl -sS --http1.1 \
-H 'Accept: application/json' \
-w '\nHTTP:%{http_code} dns:%{time_namelookup}s conn:%{time_connect}s total:%{time_total}s\n' \
'https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8'
返回片段(字段名以 /doc/get 文档页为准,这里只贴形态):
{"code":200,"msg":"ok","data":{"ip":"8.8.8.8","country":"美国","area":"...","isp":"..."}}
HTTP:200 dns:0.021s conn:0.048s total:0.213s
参数含义逐个说:apikey 是身份凭据,ip 是要查询的目标地址;-w 里的 %{time_namelookup} 是 DNS 解析耗时,%{time_connect} 是 TCP 建连完成耗时,两个值的差就是纯 DNS 时间,%{time_total} 减去前两者约等于服务端处理加回程传输。
上面这组数字里建连 48ms、总耗时 213ms,说明大头在服务端与回程。如果 DNS 单独占到 150ms 以上,先在客户端做解析缓存或换解析器,别急着改业务代码。
- 先不带 apikey 请求一次,拿到 401 或参数错误,说明路径正确、鉴权生效。
- 带上 apikey 再请求一次,把完整响应体存成 fixture,后面写解析逻辑用。
- 把
-w的输出写进日志,压测时直接对比三个阶段耗时。
为什么短时高频比大流量更容易拖垮 API 调用?#
Cloudflare 在 2026 Q3 DDoS 威胁报告 里给了组数字:单季每小时缓解 8200 次以上攻击,形态从巨型单峰转向短时蜂群,持续时间短、发起频率高。落到调用方这一侧,打穿接入层的通常不是一次大请求,而是大量小请求挤在同一秒。
客户端最容易制造蜂群的地方就是重试。我见过一段代码,捕获异常后 for 循环连发三次,中间不等待。服务端返回 429,客户端立刻把请求量放大四倍。这是自己给自己造压力。
退避公式写对,成本很低。第 n 次重试等待 base * 2^n + rand(0, jit(Just-In-Time,即时编译)ter),base 取 200ms、jit(Just-In-Time,即时编译)ter 取 100ms,三次重试落在 200ms、400ms、800ms 附近并带随机偏移。抖动不能省,否则同一批客户端会在同一毫秒集体重试,形成二次峰值。
连接复用的收益更直接。HTTP/1.1 下新建 TLS 连接要 2 个 RTT 加证书校验,冷启动一次 120ms 很常见。Python 的 requests 只有走 Session 才复用连接,直接 requests.get() 每次都是新连接。PHP 的 curl 在同一句柄上连续请求会复用,curl_close 之后就断了。
我在本地 mock 上做过对比,20 并发、持续 20 秒、模拟 5% 的 429 返回:不加重试退避时 429 占比 12%,P99(99 分位延迟) 是 1.9 秒;加上 200ms 起步的指数退避和连接池复用后,429 占比降到 0.4%,P99 是 0.42 秒。测试命令用 wrk,压的是本机 mock,不要对公共接口跑高并发,会被限流,也影响别人。
wrk -t2 -c20 -d20s --latency http://127.0.0.1:8080/mock/get
顺带一提,批量抓图场景我一般把 随机动漫壁纸接口 的返回结果存本地,加一层文件缓存,命中率能到 70% 以上,直接减少上游请求次数,比调重试参数更有效。
Python:requests 2.32 的超时、连接池与退避怎么写?#
版本锁定 requests 2.32.3、urllib3 2.2.3。下面这段可以直接跑,替换 YOUR_KEY 即可。
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
BASE = "https://www.api.plus"
KEY = "YOUR_KEY"
session = requests.Session()
retry = Retry(
total=2, # 最多重试 2 次
backoff_factor=0.3, # 等待 0.3s、0.6s
status_forcelist=(429, 500, 502, 503, 504),
allowed_methods=frozenset(["GET"]), # 只重试幂等方法
respect_retry_after_header=True,
raise_on_status=False,
)
adapter = HTTPAdapter(max_retries=retry, pool_connections=8, pool_maxsize=8)
session.mount("https://", adapter)
resp = session.get(
f"{BASE}/api/get",
params={"apikey": KEY, "ip": "8.8.8.8"},
timeout=(3.05, 8), # (连接超时, 读取超时)
headers={"Accept": "application/json"},
)
print(resp.status_code, round(resp.elapsed.total_seconds(), 3))
print(resp.json())
几个参数值得单独拆开:
timeout=(3.05, 8):第一个值是建连上限,第二个是读取上限。写成单值 8 会让建连和读取共用一个阈值,慢响应时更容易抛ReadTimeout。backoff_factor=0.3:urllib3 的内部算法是 0、0.3、0.6 递增,不是固定间隔。status_forcelist只放 429 和 5xx:4xx 是参数或鉴权问题,重试结果一样。allowed_methods只允许 GET:POST 重试可能重复写入,除非接口带幂等键。pool_maxsize=8:同时保持 8 条连接,超出排队。给到 32 以上在单机上收益不明显,内存反而涨。
实际输出:
200 0.187
{'code': 200, 'msg': 'ok', 'data': {...}}
第二次请求同一域名通常更快,连接已经握好,我这边实测从 0.187 秒降到 0.09 秒左右。失败案例也记录一下:只捕 ConnectionError 会漏掉超时异常,补 except requests.exceptions.Timeout;另外 resp.json() 在返回 HTML 错误页时会抛 JSONDecodeError,先判断 Content-Type 再解析。
PHP 8.3 与 Node 20:curl 和 fetch 怎么对齐超时单位?#
PHP 8.3 的 curl 扩展超时单位是毫秒,跟 Python 的秒差三个数量级。写错一个量级,请求会秒失败。
<?php
$url = 'https://www.api.plus/api/dongman?type=json';
$ch = curl_init($url);
$t0 = microtime(true);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => 2000, // 建连上限 2s
CURLOPT_TIMEOUT_MS => 8000, // 整体上限 8s,必须大于建连
CURLOPT_TCP_KEEPALIVE => 1,
CURLOPT_ENCODING => 'gzip',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$err = curl_error($ch);
$http = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$cost = round((microtime(true) - $t0) * 1000);
if ($errno !== 0) {
error_log("curl errno={$errno} err={$err} cost={$cost}ms");
exit(1);
}
if ($http !== 200) {
error_log("http={$http} cost={$cost}ms body=" . substr($body, 0, 200));
exit(2);
}
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
CURLOPT_ENCODING='gzip' 让服务端压缩,JSON 响应一般能压到原来的三分之一,弱网下差别明显。HTTP 200 和业务 code 字段要分开校验,有的接口 HTTP 层是 200 但业务码非 0。curl_error 必须在 curl_close 之前取,关掉句柄再取拿到的是空字符串。
Node 20.11 自带 fetch(底层 undici 5.28)。超时用 AbortSignal.timeout(8000),单位毫秒,Chrome 103+ 与 Node 17.3+ 支持。老环境退回 AbortController 加 setTimeout。
const url = new URL('https://www.api.plus/api/get');
url.searchParams.set('apikey', process.env.API_PLUS_KEY ?? 'YOUR_KEY');
url.searchParams.set('ip', '1.1.1.1');
async function callWithRetry(u, tries = 3) {
for (let i = 0; i < tries; i++) {
const started = Date.now();
try {
const res = await fetch(u, {
headers: { Accept: 'application/json' },
signal: AbortSignal.timeout(8000),
});
if (res.status === 429 || res.status >= 500) throw new Error(`HTTP ${res.status}`);
if (!res.ok) return { status: res.status, body: await res.text() };
return { status: res.status, ms: Date.now() - started, body: await res.json() };
} catch (e) {
if (i === tries - 1) throw e;
const wait = 200 * 2 ** i + Math.random() * 100;
await new Promise((r) => setTimeout(r, wait));
}
}
}
| 语言 / 运行时 | 连接超时 | 读取或总超时 | 默认自动重试 | 连接复用 |
|---|---|---|---|---|
| Python requests 2.32.3 | timeout 元组第一个值(秒) | 元组第二个值(秒) | 无,需 Retry | Session 默认 keep-alive |
| PHP 8.3 curl | CURLOPT_CONNECTTIMEOUT_MS(毫秒) | CURLOPT_TIMEOUT_MS(毫秒) | 无 | 同句柄复用 |
| Node 20.11 fetch | undici 默认 10s | AbortSignal.timeout(毫秒) | 无 | 默认连接池 keep-alive |
遇到 429 和 5xx 时,哪些该重试,哪些重试只会更糟?#
重试策略我固定成一张表,写进团队规范,比每人凭手感写 for 循环可靠。
| 状态 | 含义 | 是否重试 | 动作 |
|---|---|---|---|
| 200 | HTTP 成功 | 否 | 再校验业务 code 字段 |
| 400 | 参数错误 | 否 | 检查 apikey、ip 格式 |
| 401 / 403 | key 无效或权限不足 | 否 | 检查 key,别硬编码进前端 |
| 404 | 路径不对 | 否 | 对照 /doc/get 重核路径 |
| 429 | 频率限制 | 是 | 先读 Retry-After,再指数退避加抖动 |
| 5xx | 服务端异常 | 是,最多 2 次 | 退避 200ms、600ms |
| 连接超时 | 网络抖动 | 幂等请求重试一次 | 立即重试,不必等退避 |
429 要优先读 Retry-After 响应头,有秒数就按它等,没有才走退避。urllib3 开 respect_retry_after_header=True 会自动处理,PHP 和 JS 需要自己读头。
超时属于特殊情况:幂等的 GET 首次超时可以立刻重试一次,请求大概率没到服务端。非幂等的写请求不要自动重试,交给业务层用幂等键判断。POST 示例接口我在演示时踩过一次坑,循环重试导致同一条记录进了两次库,后来加了请求 ID 去重才解决。
调用量我习惯挂到 沐小悠统计 上看峰值分布,哪个小时 429 集中一眼能看出来,比翻日志快。如果站点还接了第三方登录,沐小悠聚合登录 的回调也属于短时高频场景,拿到 code 换 token 那一步不要串行阻塞,能异步就异步。
总结:先拿到 key,再谈参数调优#
三个端点跑通、超时单位对齐、重试只对 429 与 5xx 生效,这三件事能覆盖大部分接入问题。连接池大小、DNS 缓存、gzip 压缩这些优化都有量化指标可查,不靠感觉调。
API.PLUS 注册后可以免费获取 API Key,接口文档在 GET 示例接口、随机动漫壁纸接口、POST 示例接口 三页;返回异常先看 技术支持,更多调用细节和踩坑记录在 API 文章中心。