Skip to content

Adapters

HadesTop edited this page Aug 21, 2026 · 5 revisions

适配器

适配器决定「这个请求实际怎么发出去」。同一份调用代码,换个 adapter= 就换了底层实现。

一览

名字 装法 适合 特点
curl_cffi 默认自带 绝大多数场景 唯一带 TLS 指纹伪装;HTTP/1.1 + HTTP/2
niquests pip install "ipclick[niquests]" 需要 HTTP/3、或纯 API 调用 HTTP/1.1 + HTTP/2 + HTTP/3 (QUIC);requests 兼容 API
browser 浏览器渲染 内容要 JS 跑完才有 通用名,引擎由服务端 [BROWSER].engine
camoufox / patchright / playwright / DrissionPage 同上 要点名某个引擎时 浏览器渲染

请求级选择:

d.get(url)                        # curl_cffi
d.get(url, adapter="niquests")
d.get(url, adapter="browser")     # 引擎由服务端定 —— 推荐这么写
d.get(url, adapter="camoufox")    # 点名,服务端没装就报 AdapterError

curl_cffi —— 默认

基于 libcurl-impersonate。它的价值在于TLS 指纹:很多站点在 TLS 握手阶段就能看出 「这不是浏览器」,此时改 User-Agent 是没用的,因为 JA3/JA4 指纹对不上。curl_cffi 直接复刻真实浏览器的 ClientHello。

d.get(url, impersonate="chrome124")

不传的话 SDK 会自动填 impersonate="chrome"(由 curl_cffi 解析成它当前版本的 Chrome 指纹)。 可选值跟随 curl_cffi 版本(chrome* / safari* / edge* 等)。

只有 curl_cffi 支持 impersonate 别的适配器收到这个参数会报错而不是忽略—— 静默忽略的话你以为伪装开着,实际裸奔,被封了都不知道为什么。

niquests —— HTTP/2 与 HTTP/3

requests 的现代继任者,API 几乎一样。

d.get(url, adapter="niquests")

用它的两种情形:

  1. 目标站点走 HTTP/3。QUIC 在移动网络和高丢包链路上明显更快。
  2. 对方不做指纹检测(自家 API、公开接口),此时不需要 curl_cffi 的伪装开销。

requestshttpx 两个适配器不存在——它们的能力被 niquests 覆盖,且 niquests 还多支持 HTTP/3,维护两套等价实现不划算。传这两个名字会得到一条明确的迁移提示 ("请改用 niquests"),而不是笼统的"不支持"。protobuf 枚举值保留并标了 deprecated, 不会被复用——老客户端连上来时报的是有用的话。

undetected_chromedriver 也带一条专门的提示,但它的说法是不会实现而不是"还没做": 能力与 patchright / camoufox 重叠。要反检测的 Chromium 用 [patchright], 要最彻底的指纹伪装用 [camoufox]

通用 vs 点名

请求 adapter="browser"通用写法:客户端只表达「我要渲染」,具体引擎由服务端的 [BROWSER].engine 决定。好处是客户端代码不用关心每台节点装了什么——Windows 节点用 DrissionPage、Linux 节点用 camoufox,同一份调用代码都能跑。

点名(adapter="camoufox")表达的是「我就要这个引擎」。服务端没装就直接失败—— 这也是对的:你要的是 camoufox 的反检测特性,悄悄换成 playwright 只会让你以为它生效了。

所有适配器共享的行为

不管用哪个适配器,下面这些都一样——它们收敛在共享的一层里,而不是各写一遍: 重试是统一的 @retry() 装饰器(adapters/retry.py),按 host 限流则在服务层统一施加。

重试

[DOWNLOADER.retry]
max_attempts = 3
initial_backoff = 1
backoff_exponent = 2.0
max_backoff = 30                          # 硬上限 300
retry_codes = [429, 500, 502, 503, 504]

等待时间 = initial_backoff × exponent^已重试次数,封顶 max_backoff, 最后再乘一个 0.8~1.2 的随机抖动(避免同一批请求齐步重试,把下游又打一遍)。 抖动施加在封顶之后,所以单次等待的真实上界是 max_backoff × 1.2—— 默认 max_backoff = 30 时能等到 36 秒。 连接层异常总是重试,与 retry_codes 无关。

不会重试的东西AdapterError(依赖缺失、引擎未就绪)和 ValidationError (参数不合法、脚本语法错、永久性导航错误)。这些重试多少次都是同一个结果, 只会把失败反馈拖慢好几倍。

这两类映射到不同的 gRPC 码——AdapterErrorFAILED_PRECONDITIONValidationErrorINVALID_ARGUMENT——所以只写 except AdapterError 会漏掉一半。

单次请求可以覆盖:

d.get(url, max_retries=0, timeout=5)

allowed_status_codes

d.get(url, allowed_status_codes=[200, 503])

它只做一件事:把列进来的状态码从重试名单里摘掉。所以只对本来会重试的码 (默认 429/500/502/503/504)有意义——上面这个例子是说"503 就 503,别重试了"。

三处容易误解,说清楚:

  • 不传时的默认值是 [200, 404],不是空。所以照抄 allowed_status_codes=[200, 404] 是个空操作。
  • 404 本来就不重试,列上它省不下任何一次。
  • 它不改变成功判定。 摘掉之后 resp.ok / is_success() 仍然严格只认 2xx, 503 不会因为被"允许"就变成成功。

超时

timeout单次尝试的上限,不是总时长。带重试时最坏总耗时约 timeout × (max_retries + 1) + 退避总和。集群转发的默认超时就是按这个公式推的 (见集群)。

按 host 限流

并发闸门与 QPS 令牌桶对所有适配器统一生效,见性能

会话复用与缓存上限

适配器按连接属性缓存并复用 session:curl_cffi 按 (代理, 证书校验, 指纹), niquests 按 (代理, 证书校验)。同一组合的请求共用一个连接池——也共用一个 cookie jar, 这一点见 README 的「已知限制」。

缓存有 LRU 上限(64),超出后关掉最久未用的那个。有上限是必须的:粘性会话代理的常规 用法是每个请求带一个不同的会话 ID(http://user-Csess<随机>:pw@gw:8000),而它是 key 的一部分——不淘汰的话每个请求都留下一个 session,各自持有连接池与 keep-alive 连接, 一万次请求就是上 GB 常驻加一堆 fd,进程活着就回收不掉。

刻意把会话凭据从代理串里归一化掉:不同凭据对应不同出口 IP,共用一个 session 会串连接、把粘性会话本身破坏掉。

装没装:现查,不用重启

"这台机器上哪些适配器能用"走 importlib.util.find_spec() 探测——不执行模块代码, 纯文件系统级。探测结果带缓存,Web 管理端的「刷新状态」会清掉这份缓存重新探测。 带来两个实际差别:

  • 在终端里 pip install "ipclick[niquests]" 之后,Web 管理端点一下「刷新状态」 就能用了,不用重启服务
  • 卸载也能正确反映。这里不能用模块级 try: import——真 import 过的模块留在 sys.modules 里,删掉磁盘上的包也不会让它消失,于是"装"看得见、"卸"看不见。

真正的 import 仍然在执行路径上(第一次构造该适配器时),这一层只负责状态展示。

「试一试」的适配器下拉框也跟着变了:没装的不再从列表里消失,而是置灰并标上安装 命令。消失会让对着文档看的人以为文档和实现对不上,也不知道 IPClick 到底支持哪些。 通用占位值 browser 单独标注"自动选择引擎",不和真实适配器名混排——它不是第六个 可选组件。

装 / 卸也可以直接在 /components 页点按钮完成,见 Web 管理端 · 组件

自定义适配器

from ipclick.adapters.base import DownloaderAdapter
from ipclick.adapters.registry import register_adapter

from ipclick.dto.response import Response

class MyAdapter(DownloaderAdapter):
    adapter_name = "my_adapter"

    def download(self, url: str, **kwargs) -> Response: ...

register_adapter(MyAdapter)

第一个位置参数是 URL 字符串,不是 DownloadTask——本项目里 task 是另一个东西, 照着 task.url 写必然 AttributeError。返回值必须是 Response:重试装饰器 会对它取 .elapsed_ms

注册只影响服务端进程内的注册表,客户端选不中这个名字。 gRPC 上 adapter 字段是 封闭的 protobuf 枚举,没有字符串逃生口,SDK 在构造请求时就会抛"未知的适配器名称" (Web 端的「试一试」走同一条路)。所以自定义适配器眼下只有两条路能真跑起来: 在服务端进程内直接调用它,或者让它顶替一个已有枚举名(adapter_name = "niquests")。 要一个全新的名字,得改 dto/proto/task.protoAdapterType 再重新生成代码。

常见报错

报错 含义
适配器 'httpx' 已移除:请改用 niquests… 这个适配器不存在,用 niquests
适配器 'niquests' 需要额外依赖:pip install "ipclick[niquests]" 包没装
引擎 camoufox 的浏览器本体未就绪,请执行 python -m camoufox fetch 包装了、浏览器本体没下
下载器适配器 'xxx' 尚未支持,当前可用: … 名字拼错了
转发到节点 X 失败 —— UNAVAILABLE / UNAUTHENTICATED … 点名的那台连不上或集群令牌对不上
curl_cffi 之外的适配器不支持 impersonate=… 见上文

这几类说法刻意区分开:「装一下就能用」「再下个浏览器」「改配置」「改代码」「查那台机器」, 对应的处理动作完全不同。

顺带一个容易反过来记的点:forward = "off" 时在「试一试」里点名某个节点不会报错。 本机没开转发时,那一页会用集群内部令牌直连目标节点发一次请求。

下一步

Clone this wiki locally