用户登录

技术文章

外部链接将在新标签页打开。

API.PLUS 接入教程:Python、PHP、JS 调用

一句话回答

接入 API.PLUS 只需要固定鉴权参数、按接口 P95 设定超时、对 5xx 做有限退避重试。本文给出 Python、PHP、JavaScript 三份可直接运行的调用代码,并附监控告警与风险清单。

核心要点 TL;DR
  • 超时按 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/getGETapikey、ipJSON,data 内含 country / province / city / isp
随机动漫壁纸 /api/dongmanGETtype=jsonJSON 里给图片地址,或直接 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 倍余量
读超时(图片)20s4MB 图在 2Mbps 链路上要 16s 左右
重试次数23 次以上在抖动时会把上游压力放大 4 倍
退避间隔300ms 起递增固定间隔会形成同步重试波峰
4xx 处理不重试参数或密钥错了,重试一万次结果一样
429 处理读 Retry-After服务端已给出等待时间,按它的来

外部环境也在变。Cloudflare 2026 Q3 的报告里,平均每小时缓解 8,200 次以上 DDoS 攻击,攻击形态转向更短、更高频的脉冲(Cloudflare DDoS Threat Report 2026 Q3)。对客户端的含义是:短促抖动变多,重试策略要保守。固定 3 次重试在这种环境里比 2 次更容易打出重试风暴(Retry Storm)。

降级按顺序做,别一上来就返回 500:

  1. 缓存兜底:Redis 里存着上一次成功结果,直接返回并打 X-Stale: 1。
  2. 静态兜底:壁纸接口失败时返回本地默认图,HTTP 200,加 X-Fallback: 1。
  3. 熔断(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 堆积,站点整体 502CURLOPT_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,口径:仅统计已上线接口,不含扫描与无效路径),可作为容量规划与稳定性判断的现实参考。

常见问题(FAQ)

调用 API.PLUS 时 apikey 放 header 还是 query?
按现有接口约定走 query,形如 ?apikey=YOUR_KEY&ip=8.8.8.8。query 会被写进 Nginx access_log,生产环境要在日志格式里脱敏或改用变量拼接,避免密钥随日志外泄。
接口超时了应该重试几次?
连接失败与 5xx 最多重试 2 次,间隔 300ms 起递增;4xx 和业务错误码不重试。重试超过 2 次在抖动场景会放大上游压力,收益很低。
浏览器端能直接调用沐小悠API 吗?
技术上能,但不该这么做。前端直连会把 API Key 暴露在 DevTools 和打包产物里,正确做法是前端请求自己的域名,由 Nginx 或 BFF 补上 key 再转发。

参考来源

  1. JetBrains Academy – October Digest
  2. Introducing the Cloudflare PHP Capture the Flag challenge
  3. [$] LWN.net Weekly Edition for October 29, 2026
  4. Widespread PHP Flaw May Cause Many Site Outages Today

以上为本文写作时的参考资料,引用观点以原文为准。

作者:沐小悠

本文约 33 分钟读完 · 关键词:API.PLUS接入、沐小悠API调用、Python requests、PHP curl、JavaScript fetch

更多文章 GET 示例接口

「」标签下的其他文章