API.PLUS 接入教程:Python、PHP、JS 调用
接入 API.PLUS 只需要固定鉴权参数、按接口 P95 设定超时、对 5xx 做有限退避重试。本文给出 Python、PHP、JavaScript 三份可直接运行的调用代码,并附监控告警与风险清单。
- 超时按 P95 的 2 倍设,重试上限 2 次,4xx 不重试
- 图片接口必须限制响应体大小,流式下载边下边计数
- 前端不放 API Key,交给 Nginx 或 BFF 中转补参数
调通 API.PLUS(沐小悠API)就三件事:拼对鉴权参数、给请求设超时与有限重试、把错误码映射成可处理的分支。下面三份 Python、PHP、JavaScript 代码可直接跑。
先把接口形态说清楚:沐小悠API 走 HTTPS + query 参数,没有必须安装的 SDK,也不依赖特定语言运行时。这对多语言项目是好事,代价是每个语言都得自己把超时、重试、响应体大小这些边界补齐。补不齐,出问题的姿势千奇百怪。
接入前把三件事定死:鉴权参数、超时、错误码#
我在项目里见过最常见的错误是:接口能通,上线三天后上游抖动,PHP-FPM(FastCGI Process Manager)进程池被打满。根因不是接口慢,是 curl 没设超时,默认无限等。所以别急着写业务代码,先把下面这张表填好。
对比表:两个常用接口的请求与返回形态
| 接口 | 方法 | 关键参数 | 返回形态 |
|---|---|---|---|
IP 归属地查询 /api/get | GET | apikey、ip | JSON,data 内含 country / province / city / isp |
随机动漫壁纸 /api/dongman | GET | type=json | JSON 里给图片地址,或直接 302 到图床 |
三个参数的含义:apikey 是账号密钥,按现有约定走 query,不放 header;ip 传点分 IPv4 或标准 IPv6 字符串,构造 URL 时必须做百分号编码(IPv6 带冒号,不编码会截断);type=json 决定壁纸接口返回元数据还是直接重定向图片流。
可执行脚本:curl 调 IP 查询,带超时与有限重试
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
curl -sS \
--connect-timeout 2 \
--max-time 5 \
--retry 2 \
--retry-delay 1 \
--retry-connrefused \
-H "Accept: application/json" \
"https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8"
预期返回(字段名以文档为准,不同接口的 data 结构不同):
{
"code": 200,
"msg": "success",
"data": {
"ip": "8.8.8.8",
"country": "美国",
"province": "加利福尼亚",
"city": "山景城",
"isp": "Google LLC"
}
}
--connect-timeout 2 管 TCP 与 TLS 握手,跨地域 P99 通常低于 500ms,2 秒是宽松值。--max-time 5 限制整体耗时。--retry 2 默认只覆盖连接失败和 5xx,加上 --retry-connrefused 才管「连接被拒」。这三行也是后面三份代码的设计基线。
错误码分两类:HTTP 状态码和业务码。外层 code != 200 属于业务失败,通常是参数问题,重试没意义;HTTP 5xx 才是可重试的。两类别混在一个 try 里 catch,日志会很难看。更完整的字段说明看 GET 示例接口 文档页。
如何用 PHP 8.2 稳定调用 IP 查询接口?#
PHP 环境建议 8.2 或 8.3,确认 curl 扩展已开(php -m | grep curl)。PHP 的 max_execution_time 默认 30 秒,是脚本级限制,管不住 curl 自身等待,两个超时得分开设。
可执行脚本:PHP 8.2 + curl,参数校验、超时、4xx 不重试
<?php
declare(strict_types=1);
const API_PLUS_IP_ENDPOINT = 'https://www.api.plus/api/get';
function apiPlusGetIp(string $ip, int $maxRetry = 2): array
{
if (filter_var($ip, FILTER_VALIDATE_IP) === false) {
throw new InvalidArgumentException('非法 IP: ' . $ip);
}
$key = getenv('API_PLUS_KEY');
if ($key === false || $key === '') {
throw new RuntimeException('缺少 API_PLUS_KEY 环境变量');
}
$url = API_PLUS_IP_ENDPOINT . '?' . http_build_query(
['apikey' => $key, 'ip' => $ip],
'',
'&',
PHP_QUERY_RFC3986
);
$lastError = '';
for ($i = 0; $i <= $maxRetry; $i++) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => 2000,
CURLOPT_TIMEOUT_MS => 5000,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$errno = curl_errno($ch);
$errText = curl_error($ch);
if ($errno === 0 && $httpCode === 200 && is_string($body)) {
$json = json_decode($body, true);
if (is_array($json) && ($json['code'] ?? 0) === 200) {
return is_array($json['data'] ?? null) ? $json['data'] : [];
}
$lastError = 'business_code_' . ($json['code'] ?? 'null');
} else {
$lastError = sprintf('http=%d errno=%d %s', $httpCode, $errno, $errText);
if ($httpCode >= 400 && $httpCode < 500) {
break; // 参数错、密钥错,重试无意义
}
}
if ($i < $maxRetry) {
usleep((int) (300000 * ($i + 1))); // 300ms、600ms 递增退避
}
}
throw new RuntimeException('IP 查询失败: ' . $lastError);
}
// 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
try {
$row = apiPlusGetIp('8.8.8.8');
header('Content-Type: application/json; charset=utf-8');
echo json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
} catch (Throwable $e) {
error_log('[api.plus] ' . $e->getMessage());
http_response_code(503);
echo json_encode(['error' => 'upstream_unavailable']);
}
几个细节单独说。第一,http_build_query 第四参传 PHP_QUERY_RFC3986,空格编成 %20 而不是 +,部分网关对 + 处理不一致。第二,CURLOPT_CONNECTTIMEOUT_MS 与 CURLOPT_TIMEOUT_MS 都是毫秒,别写成秒。第三,函数返回 array 而不是对象,避免调用方在 PHP 8 的严格类型下到处 -> 报错。
失败案例:同一段代码放进常驻进程(Swoole、RoadRunner)会出问题。usleep阻塞整个 worker,重试两次等于把 600ms 的等待成本乘上并发数。常驻模式下要么换异步客户端,要么把maxRetry压到 1。
PHP 这套运行时本身也需要盯补丁。2026 年 10 月出现过影响面很大的 PHP 核心漏洞,约七成站点被要求升级核心软件(KrebsOnSecurity)。跑 API 调用的机器同样在攻击面里,composer audit 和系统包更新别跳过。
Python 拉随机动漫壁纸,为什么必须限制响应体大小?#
文本接口和图片接口不是一个量级。IP 查询返回 1KB 上下,一张竖版壁纸 300KB 到 4MB,超宽图能过 10MB。不设上限,几十个并发就能把容器内存打满,表现是进程被 OOM Killer 干掉,日志里什么都看不到。
环境用 Python 3.11 及以上、requests 2.32.x。timeout 别只写一个数字,写元组区分连接与读取。
可执行脚本:随机壁纸拉取,元数据 + 流式下载 + 字节上限
import os
import time
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
DONGMAN_URL = "https://www.api.plus/api/dongman"
MAX_BYTES = 15 * 1024 * 1024 # 单张图上限 15MB
def build_session() -> requests.Session:
retry = Retry(
total=2,
connect=2,
read=1,
backoff_factor=0.3,
status_forcelist=(429, 500, 502, 503, 504),
allowed_methods=frozenset({"GET"}),
raise_on_status=False,
)
sess = requests.Session()
sess.mount("https://", HTTPAdapter(max_retries=retry, pool_connections=8, pool_maxsize=16))
sess.headers.update({"User-Agent": "api-plus-demo/1.0"})
return sess
def fetch_wallpaper(sess: requests.Session, timeout=(2.0, 6.0)):
params = {"type": "json"}
key = os.environ.get("API_PLUS_KEY")
if key:
params["apikey"] = key
meta = sess.get(DONGMAN_URL, params=params, timeout=timeout)
meta.raise_for_status()
payload = meta.json()
if payload.get("code") != 200:
raise RuntimeError(f"业务错误: code={payload.get('code')} msg={payload.get('msg')}")
url = payload["data"]["url"]
if not url.startswith("https://"):
raise ValueError(f"非 https 资源,拒绝下载: {url[:60]}")
started = time.perf_counter()
with sess.get(url, timeout=(2.0, 20.0), stream=True) as img:
img.raise_for_status()
declared = int(img.headers.get("Content-Length") or 0)
if declared > MAX_BYTES:
raise ValueError(f"Content-Length {declared} 超过上限 {MAX_BYTES}")
buf = bytearray()
for chunk in img.iter_content(64 * 1024):
buf.extend(chunk)
if len(buf) > MAX_BYTES: # 没有 Content-Length 时靠这里兜底
raise ValueError("流式下载超限,已中断")
cost_ms = (time.perf_counter() - started) * 1000
return url, bytes(buf), cost_ms
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
if __name__ == "__main__":
session = build_session()
try:
img_url, blob, cost = fetch_wallpaper(session)
print(f"{img_url} {len(blob) / 1024:.1f}KB {cost:.0f}ms")
except (requests.RequestException, ValueError, KeyError, RuntimeError) as exc:
print(f"fallback: {exc}")
# 生产环境在这里读本地缓存图,或 302 到 CDN 兜底地址
预期输出类似:https://.../2026/10/xxxx.webp 842.3KB 614ms。三段分别是图床地址、实际字节数、下载耗时。这三个值都值得进日志——「元数据慢」和「图片慢」是两类故障,混在一起排查会绕远路。
三个边界点值得记:
timeout=(2.0, 6.0)是 (连接, 读取) 元组,写单个数字会让连接和读取共用同一个上限。Retry只对 GET 与 429/5xx 生效,raise_on_status=False把 4xx 交给上层判断,避免重试把参数错误放大。stream=True加iter_content(64*1024),边下边计数,超限立刻断流,内存占用封在 15MB 以内。
批量取图别在循环里反复打同一个 URL。加一层 Redis(Remote Dictionary Server)缓存,键写成 api:dongman:{type},TTL 300 秒;或者看 随机动漫壁纸接口 是否支持一次返回多条,减少往返次数。
JavaScript 在 Node 与浏览器里调用有什么区别?#
Node 18 起全局有 fetch,建议直接用 20 LTS 或更高。写法上和浏览器几乎一致,硬区别只有一个:浏览器里不能出现 API Key。剩下都是超时控制的手段差异。
可执行脚本:Node 20 + fetch,AbortController 超时与指数退避
const ENDPOINT = 'https://www.api.plus/api/get';
const RETRIES = 2;
const TIMEOUT_MS = 5000;
async function queryIp(ip, { retries = RETRIES, timeoutMs = TIMEOUT_MS } = {}) {
if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) throw new Error(`非法 IPv4: ${ip}`);
const key = process.env.API_PLUS_KEY;
if (!key) throw new Error('缺少 API_PLUS_KEY 环境变量');
const url = new URL(ENDPOINT);
url.searchParams.set('apikey', key);
url.searchParams.set('ip', ip);
let lastErr;
for (let attempt = 0; attempt <= retries; attempt++) {
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), timeoutMs);
try {
const res = await fetch(url, {
headers: { Accept: 'application/json' },
signal: ac.signal,
});
if (!res.ok) {
const err = new Error(`http_${res.status}`);
err.retryable = res.status >= 500 || res.status === 429;
throw err;
}
const body = await res.json();
if (body.code !== 200) {
const err = new Error(`biz_${body.code}`);
err.retryable = false;
throw err;
}
return body.data;
} catch (e) {
lastErr = e;
const retryable = e.retryable ?? e.name === 'AbortError';
if (!retryable || attempt === retries) break;
await new Promise((r) => setTimeout(r, 300 * 2 ** attempt));
} finally {
clearTimeout(timer);
}
}
throw lastErr;
}
// 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
queryIp('8.8.8.8')
.then((d) => console.log(JSON.stringify(d)))
.catch((e) => {
console.error('[api.plus]', e.message);
process.exitCode = 1;
});
setTimeout + AbortController 是 Node 18 到 20 的通用写法;AbortSignal.timeout(ms) 更短,但低版本运行时没有,团队里混版本容易踩。退避用 300 * 2 ** attempt,300ms、600ms,比固定间隔更能避开瞬时拥塞。
浏览器侧的正确姿势是自己搭中转。前端只请求同域路径,服务端补 key 再转发。
规范模板:Nginx 反代补 key,2 秒建连、5 秒读超时
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
# /etc/nginx/conf.d/api-plus.conf
resolver 127.0.0.1 valid=30s ipv6=off;
location = /proxy/ip {
set $upstream "https://www.api.plus/api/get?apikey=YOUR_KEY&ip=$arg_ip";
proxy_pass $upstream;
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
proxy_next_upstream error timeout http_502 http_503;
proxy_ssl_server_name on;
add_header X-Cache-Status $upstream_cache_status always;
}
两个坑。proxy_pass 里带变量必须配 resolver,否则 Nginx 启动直接报 no resolver defined to resolve www.api.plus。另外这段只适合内网工具,公网暴露前要用 map 对 $arg_ip 做 IPv4 白名单校验,否则就是一个敞开的口子。中转的签名与 Cookie 方案,之前整理在 API 文章中心 的《小程序与 H5 调用第三方 API:密钥不上前端的中转方案》。
超时、重试与降级参数怎么定?#
「超时按接口 P95 的 2 倍设」,这条比任何模板都好用。
对比表:客户端参数建议值与理由
| 参数 | 建议值 | 理由与取舍 |
|---|---|---|
| 连接超时 | 2s | 跨地域 TCP + TLS 握手 P99 一般低于 500ms |
| 读超时(文本) | 5s | 接口 P95 通常在 300~800ms,留 6 倍余量 |
| 读超时(图片) | 20s | 4MB 图在 2Mbps 链路上要 16s 左右 |
| 重试次数 | 2 | 3 次以上在抖动时会把上游压力放大 4 倍 |
| 退避间隔 | 300ms 起递增 | 固定间隔会形成同步重试波峰 |
| 4xx 处理 | 不重试 | 参数或密钥错了,重试一万次结果一样 |
| 429 处理 | 读 Retry-After | 服务端已给出等待时间,按它的来 |
外部环境也在变。Cloudflare 2026 Q3 的报告里,平均每小时缓解 8,200 次以上 DDoS 攻击,攻击形态转向更短、更高频的脉冲(Cloudflare DDoS Threat Report 2026 Q3)。对客户端的含义是:短促抖动变多,重试策略要保守。固定 3 次重试在这种环境里比 2 次更容易打出重试风暴(Retry Storm)。
降级按顺序做,别一上来就返回 500:
- 缓存兜底:Redis 里存着上一次成功结果,直接返回并打
X-Stale: 1。 - 静态兜底:壁纸接口失败时返回本地默认图,HTTP 200,加
X-Fallback: 1。 - 熔断(Circuit Breaker):连续 20 次失败后打开 30 秒,期间全部走兜底,不再打上游。
监控与告警清单#
指标分两层:网络层看状态码和耗时,业务层看 code != 200。只盯 HTTP 200 会漏掉「接口通了但返回错误码」这种最常见的情况。
风险清单:必采指标、阈值与告警动作
| 指标 | 采集方式 | 建议阈值 | 告警动作 |
|---|---|---|---|
| 请求 P95 延迟 | 客户端埋点 | > 800ms 持续 5min | 通知值班,比对上游状态 |
| 综合错误率 | 失败计数 / 总请求 | > 2% 持续 5min | 打开熔断,切兜底 |
| 业务码异常占比 | code != 200 计数 | > 1% | 检查参数与密钥 |
| 429 命中数 | 状态码统计 | 5min 内 > 5 次 | 降 QPS,检查是否共用密钥 |
| 超时次数 | 异常分类 | 5min 内 > 10 次 | 排查网络与上游负载 |
| 响应体超限中断 | 下载中断计数 | > 0 | 复核上限值是否定得太紧 |
| 缓存命中率 | Redis 命中计数 | < 60% | 检查 TTL 与键设计 |
风险清单#
风险清单:风险项、影响与规避方式
| 风险 | 影响 | 规避方式 |
|---|---|---|
| API Key 写进前端产物 | 被提取后盗刷,配额瞬间耗尽 | 前端只打自己域名,Nginx 或 BFF 补 key |
| 未设超时 | PHP-FPM worker 堆积,站点整体 502 | CURLOPT_TIMEOUT_MS、timeout=(2,5) |
| 无上限重试 | 重试风暴,故障被放大 | 上限 2 次 + 退避 + 熔断 |
| 响应体无大小限制 | 内存打满,进程被 OOM 杀掉 | Content-Length 校验 + 流式截断 |
| 关闭 TLS 证书校验 | 中间人攻击,密钥明文外泄 | 保持默认校验,不要设 verify=False |
| key 进了 Git 历史 | 等同已泄露 | 立刻轮换,不要只删 commit |
密钥泄露的时间窗比多数人想的短。AWS 在 10 月的安全服务公告里提到,从漏洞披露到可用利用代码出现已经压到小时级(AWS News Blog)。泄露的 key 一旦被扫到,几小时内就能跑满配额。发现泄露先轮换,再谈清理历史。
总结#
三份代码的骨架是一样的:参数校验 → 带超时的请求 → 有限退避重试 → 错误分类 → 兜底返回。语言不同,这五步不换。真正区分「能跑」和「能上生产」的,是那几个具体数字:2 秒连接、5 秒读取、2 次重试、15MB 上限、300ms 退避起点。
落地时的建议很简单:先把 curl 调通,确认返回结构,再用对应语言的示例替换;把 key 放环境变量;把超时和重试当成必填项写进代码评审清单。做完这三步,剩下的都是业务逻辑。
适用范围:
- 服务端同步调用文本类接口(IP 查询、元数据查询),并发在单机几百 QPS 以内。
- 需要拉取图片、文件等大响应体的下载场景,且能接受流式处理。
- 技术栈为 PHP 8.2+、Python 3.11+、Node 18+ 的新项目或改造中的存量项目。
不适用范围:
- 需要长连接或流式推送(SSE、WebSocket)的接口,本文的超时与重试模型不适用。
- 强一致场景:缓存兜底会返回 stale 数据,不能用于实时风控决策。
- 无出网权限的内网环境,Nginx 反代方案需要在有出网能力的边界节点部署。
本站实测数据(近 24 小时)#
以下数据来自 API.PLUS 线上调用日志(统计时间 2026-10-30 17:36,口径:仅统计已上线接口,不含扫描与无效路径),可作为容量规划与稳定性判断的现实参考。
- 接口调用总数:141 次;成功返回 139 次,鉴权/参数校验拦截 2 次
- 最活跃接口:随机动漫壁纸(95 次)、Menhera 动漫图片(41 次)、IP 地址查询(4 次)
- 近 7 天累计调用:900 次
常见问题(FAQ)
调用 API.PLUS 时 apikey 放 header 还是 query?
接口超时了应该重试几次?
浏览器端能直接调用沐小悠API 吗?
参考来源
- JetBrains Academy – October Digest
- Introducing the Cloudflare PHP Capture the Flag challenge
- [$] LWN.net Weekly Edition for October 29, 2026
- Widespread PHP Flaw May Cause Many Site Outages Today
以上为本文写作时的参考资料,引用观点以原文为准。