-
Notifications
You must be signed in to change notification settings - Fork 0
Adapters
适配器决定「这个请求实际怎么发出去」。同一份调用代码,换个 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基于 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。 别的适配器收到这个参数会报错而不是忽略—— 静默忽略的话你以为伪装开着,实际裸奔,被封了都不知道为什么。
requests 的现代继任者,API 几乎一样。
d.get(url, adapter="niquests")用它的两种情形:
- 目标站点走 HTTP/3。QUIC 在移动网络和高丢包链路上明显更快。
- 对方不做指纹检测(自家 API、公开接口),此时不需要 curl_cffi 的伪装开销。
requests和httpx两个适配器不存在——它们的能力被 niquests 覆盖,且 niquests 还多支持 HTTP/3,维护两套等价实现不划算。传这两个名字会得到一条明确的迁移提示 ("请改用 niquests"),而不是笼统的"不支持"。protobuf 枚举值保留并标了deprecated, 不会被复用——老客户端连上来时报的是有用的话。
undetected_chromedriver也带一条专门的提示,但它的说法是不会实现而不是"还没做": 能力与 patchright / camoufox 重叠。要反检测的 Chromium 用[patchright], 要最彻底的指纹伪装用[camoufox]。
请求 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 码——AdapterError 是 FAILED_PRECONDITION,
ValidationError 是 INVALID_ARGUMENT——所以只写 except AdapterError 会漏掉一半。
单次请求可以覆盖:
d.get(url, max_retries=0, timeout=5)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) + 退避总和。集群转发的默认超时就是按这个公式推的
(见集群)。
并发闸门与 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.proto 的 AdapterType 再重新生成代码。
| 报错 | 含义 |
|---|---|
适配器 'httpx' 已移除:请改用 niquests… |
这个适配器不存在,用 niquests |
适配器 'niquests' 需要额外依赖:pip install "ipclick[niquests]" |
包没装 |
引擎 camoufox 的浏览器本体未就绪,请执行 python -m camoufox fetch |
包装了、浏览器本体没下 |
下载器适配器 'xxx' 尚未支持,当前可用: … |
名字拼错了 |
转发到节点 X 失败 —— UNAVAILABLE / UNAUTHENTICATED … |
点名的那台连不上或集群令牌对不上 |
curl_cffi 之外的适配器不支持 impersonate=… |
见上文 |
这几类说法刻意区分开:「装一下就能用」「再下个浏览器」「改配置」「改代码」「查那台机器」, 对应的处理动作完全不同。
顺带一个容易反过来记的点:
forward = "off"时在「试一试」里点名某个节点不会报错。 本机没开转发时,那一页会用集群内部令牌直连目标节点发一次请求。