Skip to main content

基于原primp 用rust重构调整的python请求库 - The fastest python HTTP client that can impersonate web browsers

Project description

NEVER_PRIMP

基于 Rust + wreq 的高性能 Python HTTP 客户端 专为网络爬虫、浏览器指纹伪装与风控绕过设计

Python >= 3.8 PyPI version License Rust

安装 · 快速开始 · 性能原理 · 异步 · 流式响应 · 异常处理 · Cookie 管理 · 浏览器伪装 · API 参考


为什么选择 NEVER_PRIMP?

功能 NEVER_PRIMP requests httpx curl-cffi
浏览器 TLS/JA3/JA4 指纹 ✅ 100+ 配置 ✅ 有限
HTTP/2 指纹(AKAMAI)
请求头顺序精确控制
Cookie 分割(HTTP/2 风格)
跨子域名 Cookie 共享 ✅ RFC 6265
Cookie 跨会话持久化
高并发无锁 Client 共享
GIL 释放(真实并行)
真异步(asyncio,非线程池)
流式响应(增量读 body)
类型化异常(按错误类型重试)
重试策略 ✅ 内置预算
文件上传(multipart)

性能基准(测试 URL: https://www.baidu.com)

requests httpx curl-cffi never_primp
单次请求 646 ms 90 ms 122 ms 86 ms
串行 10 次 655 ms 20 ms 47 ms 19 ms
并发 100 任务 697 ms 23 ms 56 ms 20 ms

安装

pip install -U never-primp

平台支持:Linux (x86_64/aarch64) · Windows (x86_64) · macOS (x86_64/ARM64)

从源码构建

pip install maturin
maturin develop --release

快速开始

import never_primp

# 最简单的用法
r = never_primp.get("https://httpbin.org/get")
print(r.status_code, r.json())

# 带浏览器指纹的 Client
client = never_primp.Client(
    impersonate="chrome_147",
    impersonate_os="windows",
    timeout=30.0,
)
r = client.get("https://httpbin.org/headers")
print(r.json())

# 上下文管理器(退出时自动关闭连接池)
with never_primp.Client(impersonate="firefox_149") as client:
    r = client.post("https://httpbin.org/post", json={"key": "value"})
    print(r.json())

性能优化原理深度解析

这一节详细解释 never_primp 每一项性能设计的底层原理。

1. 去除 Arc<Mutex<Client>>:消灭并发瓶颈

旧版问题

// 旧版:所有线程争同一把锁
pub struct RClient {
    client: Arc<Mutex<wreq::Client>>,  // 问题所在
}

// 每次发请求:
let resp = client.lock().unwrap().request(...).send().await;
//                ^^^^^^^^^^^
//                持锁期间,其他线程全部阻塞等待!

在 Python 的 ThreadPoolExecutor 场景下,20 个线程各自调用 client.get()

线程1  ──[lock]──── request ──── [unlock]──
线程2             [等待] ────── request ──── [等待]...
线程3                       [等待] ...
                             串行化!

新版方案

wreq::Client 内部已用 Arc 包裹所有状态(连接池、Cookie Jar、配置),它实现了 Clone + Send + Sync

// 新版:直接存储,clone 是零成本的 Arc 引用计数+1
pub struct RClient {
    client: wreq::Client,   // 内部已是 Arc<Inner>
}

// 请求时 clone 传入 async 块
let client = self.client.clone();  // 仅增加引用计数,O(1),无锁
let future = async move {
    client.request(method, url).send().await
};
py.detach(|| runtime().block_on(future));  // GIL 释放后真正并行

20 个线程同时发请求:

线程1  ── clone(O1) ─── request ───────── 并行!
线程2  ── clone(O1) ─── request ───────── 并行!
线程3  ── clone(O1) ─── request ───────── 并行!
           ↑ 每个 clone 只是原子计数+1,互不干扰

性能影响:高并发场景下吞吐量从串行变为真正并行,延迟从 O(N×T) 降为 O(T)。


2. GIL 释放:Python 多线程的正确姿势

Python 的 GIL(Global Interpreter Lock)保证同一时刻只有一个线程执行 Python 字节码,但 IO 操作期间可以释放。

never_primp 的实现

// 进入 IO 之前显式释放 GIL
let wreq_response = py.detach(|| {
    runtime().block_on(future)  // 整个 HTTP 请求期间 GIL 释放
});
// GIL 在这里自动重新获取

py.detach() 等价于在 C 扩展中调用 Py_BEGIN_ALLOW_THREADS,让其他 Python 线程在等待 HTTP 响应期间正常运行。

实际效果

GIL 持有时间线(旧版无释放):
Thread1: [GIL][send HTTP][wait response][GIL] ...
Thread2:            [等待GIL..............][GIL][send HTTP]...
结果:几乎串行

GIL 释放时间线(新版):
Thread1: [GIL][send HTTP─────────]→[wait resp in Tokio]→[GIL][return]
Thread2:      [GIL][send HTTP─────]→[wait resp in Tokio]→[GIL][return]
Thread3:           [GIL][send HTTP]→[wait resp in Tokio]→[GIL][return]
结果:真正并行

GIL 只在构建请求和处理响应这两个极短的 CPU 阶段被持有,网络 IO 等待期间全部并行。


3. Tokio 异步运行时:4 Worker 线程的选择

// src/runtime.rs —— 同步与异步两条路径共用同一个运行时
pub fn init_runtime() {
    let mut builder = tokio::runtime::Builder::new_multi_thread();
    builder.enable_all().worker_threads(4).thread_name("never-primp-worker");
    pyo3_async_runtimes::tokio::init(builder);   // 在 #[pymodule] 初始化时调用一次
}
pub fn runtime() -> &'static tokio::runtime::Runtime {
    pyo3_async_runtimes::tokio::get_runtime()
}

同步路径runtime().block_on(...)异步路径AsyncClient)用 pyo3_async_runtimes::tokio::future_into_py(...) 把 Rust future 桥接到 asyncio。两者共用同一个 4-worker 运行时,几千个并发请求多路复用其上,不再每请求占用一个 OS 线程(详见 异步)。

为什么是 4 而不是更多?

Tokio 的 worker 线程处理的是 IO 事件(socket 可读/可写的通知),而不是实际等待数据。HTTP 请求的主要时间消耗在网络 RTT,不是 CPU 计算:

典型 HTTP 请求生命周期:
  建立连接: ~5ms  (CPU: <1ms)
  发送请求: ~0.1ms (CPU: <0.1ms)
  等待响应: ~50ms  (CPU: 0,纯等待)
  读取响应: ~1ms   (CPU: <0.5ms)

4 个 worker 可以同时管理数千个 "等待响应" 状态的 socket,
因为 epoll/IOCP 一次系统调用可以轮询所有就绪事件。

对于 CPU 密集的任务应该用 spawn_blocking,对于 IO 密集的请求任务,4 个 worker 通常已经饱和。增加到 16 个 worker 对 IO bound 场景几乎无提升,反而增加线程切换开销。


4. 连接池:TCP 复用的关键参数

client_builder
    .pool_max_idle_per_host(32)        // 默认:每主机保留 32 条空闲 TCP 连接(可配置)
    .pool_max_size(512)                // 默认:全局上限 512 条(可配置)
    .pool_idle_timeout(Duration::from_secs(30))  // 默认:30s 未使用则关闭(可配置)
    .tcp_keepalive(Duration::from_secs(30))       // SO_KEEPALIVE
    .tcp_keepalive_interval(Duration::from_secs(15))
    .tcp_keepalive_retries(3u32)
    .tcp_nodelay(true);                // 禁用 Nagle 算法

三个连接池参数现在可通过 Python 层按场景调整:

# 多域名爬取(默认值已适合此场景)
client = Client(impersonate="chrome_147", cookie_store=False)

# 单域名高并发(提高单 host 连接数)
client = Client(
    impersonate="chrome_147",
    pool_max_idle_per_host=64,   # 每 host 最多 64 个空闲连接
    pool_max_size=256,
    pool_idle_timeout=60.0,
)

为什么这些参数很重要?

TCP 连接复用(最大收益)

建立一条 TCP+TLS 连接的开销:

TCP 三次握手:  ~10ms (1 RTT)
TLS 1.3 握手: ~15ms (1 RTT)
合计:          ~25ms

如果每个请求都新建连接(pool 为 0),100 个请求就浪费 2500ms 在握手上。连接池让同一主机的请求复用已建立的连接,第 2 次请求开始几乎没有建连开销。

TCP Keepalive(连接稳定性)

没有 Keepalive 的问题:
  服务器/NAT/防火墙在 60-120s 内无流量会 silently drop 连接
  下次使用这条"死连接"时:RST 或超时,请求失败

SO_KEEPALIVE 工作原理:
  每 30s 发一个 TCP ACK 探测包(几乎无流量)
  服务器响应 → 连接确认活跃,重置超时计时器
  服务器无响应 → 15s 后重试,最多 3 次,才判定连接断开

效果:空闲连接保持真正可用,避免"僵尸连接"导致的请求失败

TCP_NODELAY(降低小包延迟)

Nagle 算法会把小数据包积攒到 MSS(约 1460 字节)再发送,降低延迟对 HTTP 不友好。tcp_nodelay(true) 禁用它,每次 write() 立即发送,降低请求延迟约 10-40ms。


5. Cookie Jar 的 RFC 6265 实现

wreq 的 Jar 内部结构:

HashMap<domain, HashMap<path, CookieJar>>
   ↑ 按 (domain, path) 二级索引存储

子域名 Cookie 共享(domain_match)

fn domain_match(host: &str, domain: &str) -> bool {
    host == domain                          // 完全匹配
    || (host.len() > domain.len()
        && host.ends_with(domain)
        && host.as_bytes()[host.len() - domain.len() - 1] == b'.')
    //  ↑ 确保是真子域名:api.example.com 匹配 example.com
    //    但 notexample.com 不匹配 example.com
}

查询 api.example.com 的 cookie 时,遍历所有 domain key 做 domain_match:

  • example.com → 匹配,返回其 cookie
  • api.example.com → 完全匹配,也返回

读写锁而非互斥锁

store: Arc<RwLock<HashMap<...>>>
//         ↑ 读写锁

// 查询时(大多数操作):多个线程并发读,互不阻塞
store.read().get(host)...

// 修改时(Set-Cookie 响应):独占写锁
store.write().entry(domain).or_default()...

爬虫场景下读(发请求带 Cookie)远多于写(收到 Set-Cookie),RwLock 比 Mutex 在并发读时效率高得多。

大并发内存注意事项

cookie_store=True(默认)时,所有 Set-Cookie 响应头都会写入 Jar,且永不自动过期。在高并发打多个不同域名的场景下:

  • Jar 随域名数量持续增长(无上限)
  • 每次写入(收到 Set-Cookie)都要竞争独占写锁,成为并发瓶颈

结论:无状态爬取场景使用 cookie_store=False,可显著降低内存占用和锁争用。


6. HTTP/2 连接序言合并(反检测关键)

现代风控系统会检测 HTTP/2 握手时第一次 TCP 应用数据的大小。真实浏览器(如 Safari)会将连接序言、SETTINGS、WINDOW_UPDATE、首个 HEADERS 帧合并为一次 TCP 突发

真实 Safari 握手:
  第 1 次 TCP 应用数据 [424 bytes]:
    PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n  (24 bytes, 连接序言)
    SETTINGS frame                     (51 bytes)
    WINDOW_UPDATE frame                (13 bytes)
    HEADERS frame                     (~336 bytes, 首个请求)
  → 风控检测通过 ✅

旧版实现握手(显式 flush 导致拆包):
  第 1 次 TCP 应用数据 [70 bytes]:
    PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n  (24 bytes)
    SETTINGS frame                     (51 bytes)
    WINDOW_UPDATE frame                (13 bytes)
  第 2 次 TCP 应用数据 [~298 bytes]:
    HEADERS frame
  → 风控检测失败 ❌  "[!] Safari detected but invalid SM packet (Len=70). Marking as bot."

never_primp 的底层 HTTP/2 实现移除了连接建立时的提前 flush,确保连接序言和首个请求帧在同一 TCP 段中发出,与浏览器行为完全一致。


7. 重试预算机制

// 内置令牌桶重试预算
let policy = RetryPolicy::default()
    .max_retries_per_request(2);  // 每请求最多重试 2 次

wreq 的重试策略内置了预算限制(默认 20% 额外负载):

假设发出 1000 个请求:
  正常请求: 1000
  允许的重试: 1000 × 20% = 200

如果某段时间重试过多(>200),
预算耗尽,后续请求即使失败也不再重试,
避免雪崩效应(retry storm)导致服务器更不稳定。

这比简单的 for _ in range(3): try: request() 更智能,在大并发场景下保护目标服务器。


8. 头部顺序控制(anti-bot 核心)

现代风控系统(Cloudflare、Akamai、PerimeterX)会检测请求头的顺序作为 bot 指纹:

真实 Chrome 143 的头部顺序:
  :method: GET
  :authority: example.com
  :scheme: https
  :path: /
  sec-ch-ua: ...
  sec-ch-ua-mobile: ?0
  sec-ch-ua-platform: "Windows"
  upgrade-insecure-requests: 1
  user-agent: Mozilla/5.0...
  accept: text/html,...
  sec-fetch-site: none
  sec-fetch-mode: navigate
  sec-fetch-user: ?1
  sec-fetch-dest: document
  accept-encoding: gzip, deflate, br, zstd
  accept-language: zh-CN,...

never_primp 用 OrigHeaderMap 记录头部插入顺序:

// 普通 HeaderMap 只保证 O(1) 查找,不保证顺序
// OrigHeaderMap 是有序列表,精确控制发送顺序
let mut orig_headers = OrigHeaderMap::new();
for (key, _) in client_headers.iter() {
    orig_headers.insert(key.clone());  // 保留插入顺序
}
request_builder = request_builder.orig_headers(orig_headers);

这确保了发出去的 TCP 字节流中头部顺序与真实浏览器完全一致。


Cookie 管理

client.cookies 返回一个 RequestsCookieJar 对象,提供类 dict 接口,行为与 requests.Session.cookies 一致。内部分为两层:

  • 全局 Cookie:无域名限定,随所有请求发送
  • 域名 Cookie:由 Set-Cookie 响应头自动写入,或手动通过 set(domain=…) 添加,按 RFC 6265 规则匹配

自动跨子域名共享

client = never_primp.Client(impersonate="chrome_147")

# 登录后,服务器设置 domain=.example.com 的 Cookie
client.get("https://example.com/login")  # 自动存储 Set-Cookie

# 访问子域名时,这些 Cookie 自动发送(RFC 6265)
client.get("https://api.example.com/data")   # 自动带 Cookie
client.get("https://cdn.example.com/asset")  # 自动带 Cookie

全局 Cookie 操作(类 dict)

jar = client.cookies

# 读写删(全局 Cookie,随所有请求发送)
jar["token"] = "abc"
val = jar["token"]
del jar["token"]
"token" in jar           # True / False

# 批量读写
jar.update({"a": "1", "b": "2"})
print(jar.get_dict())    # {"a": "1", "b": "2"}

# 遍历
for name, value in jar.items():
    print(name, value)

jar.clear_global()       # 只清空全局 Cookie
jar.clear_jar()          # 只清空域名 Cookie
jar.clear()              # 清空所有

域名 Cookie 操作

# 设置域名 Cookie(只发送给匹配的域名,RFC 6265)
client.cookies.set("auth_token", "eyJhbGci...",
                   domain="example.com", path="/")

# 查询会发送到某 URL 的全部 Cookie
cookies = client.cookies.get_cookies_for_url("https://api.example.com/data")

# 按名称跨域名/路径查找(适用于不知道确切 path 的情况)
val = client.cookies.find("sec_cpt")

# 查看所有域名 Cookie 的完整元数据
for name, value, domain, path in client.cookies.get_jar_cookies():
    print(f"[{domain}{path}] {name}={value}")

跨会话 Cookie 持久化

import json

# 会话一:登录
client = never_primp.Client(impersonate="chrome_147")
client.get("https://example.com/login")

# 导出并保存(包含全局 Cookie 和域名 Cookie)
with open("session.json", "w") as f:
    json.dump(client.export_cookies(), f)

# ─── 程序重启 ───

# 会话二:恢复登录状态
client2 = never_primp.Client(impersonate="chrome_143")
with open("session.json") as f:
    client2.import_cookies([tuple(c) for c in json.load(f)])

# 直接访问需要登录的页面
r = client2.get("https://example.com/dashboard")

浏览器指纹伪装

支持的浏览器(100+ 配置)

浏览器 版本范围 别名
Chrome 100–148 "chrome" → 最新
Edge 101–148 "edge" → 最新
Firefox 109–151 "firefox" → 最新
Safari macOS 15.3–26.4 "safari" → 最新
Safari iOS 16.5–26.2 "safari_ios" → 最新
Safari iPad 18–26.2 "safari_ipad" → 最新

Safari 26+ TLS 说明:Safari 26.x 使用 BoringSSL 后端,相比旧版有以下变化:

  • Cipher suites 顺序调整为 AES-256-GCM → ChaCha20 → AES-128-GCM
  • 支持 X25519MLKEM768 后量子混合密钥交换(supported_groups + key_share
  • supported_versions 仅包含 TLS 1.2 和 TLS 1.3(不再宣告 TLS 1.0/1.1)
  • accept-encoding 值更新为 gzip, deflate, br, zstd(新增 zstd) | Opera | 116–131 | "opera" → 最新 | | OkHttp | 3.9–5 | "okhttp" → 最新 |

伪装内容

client = never_primp.Client(
    impersonate="chrome_143",
    impersonate_os="windows",   # windows / macos / linux / android / ios
)

每个配置包含:

  • TLS 指纹:cipher suites 顺序、TLS extensions、椭圆曲线、签名算法(JA3/JA4)
  • HTTP/2 指纹:SETTINGS 帧参数、WINDOW_UPDATE、HEADERS 帧顺序(AKAMAI 指纹)
  • 请求头集合:与该浏览器版本完全一致的默认头部(含正确的 accept-encoding 值与位置)
  • 请求头顺序:精确匹配浏览器的头部发送顺序
  • H2 连接预热合并:连接序言 + SETTINGS + WINDOW_UPDATE 与首个 HEADERS 帧合并为单次 TCP 突发,与真实浏览器行为一致

Cookie 分割(HTTP/2 浏览器行为)

# HTTP/2 中浏览器将每个 Cookie 作为独立的 Header Frame 字段发送
client = never_primp.Client(
    impersonate="chrome_143",
    split_cookies=True,     # 默认 True(Python 层)
)

# 发出的 HTTP/2 HEADERS 帧:
# cookie: session=abc
# cookie: user_id=123
# cookie: csrf_token=xyz

异步 (AsyncClient)

AsyncClient真正的异步:每个请求返回一个由 Rust future 桥接(经 pyo3-async-runtimes)到当前 asyncio 事件循环的 awaitable,由共享的 4-worker Tokio 运行时驱动。成千上万的并发请求在少量 worker 上多路复用,不再每个请求占用一个 OS 线程——asyncio.gather 的并发量远超旧版线程池包装(旧版 run_in_executor 默认上限约 32)。

接口与同步 Client 完全一致,只是方法需 await

import asyncio
import never_primp

async def main():
    async with never_primp.AsyncClient(impersonate="chrome_147") as client:
        # 单个请求
        r = await client.get("https://httpbin.org/get")
        print(r.status_code, r.json())

        # 大并发:一次性发起成百上千个请求
        urls = [f"https://httpbin.org/anything/{i}" for i in range(500)]
        results = await asyncio.gather(
            *(client.get(u) for u in urls),
            return_exceptions=True,   # 单个失败不影响整体
        )
        ok = sum(1 for x in results if not isinstance(x, Exception))
        print(f"{ok}/{len(urls)} 成功")

asyncio.run(main())

并发轮换指纹/代理同样适用:await client.get(url, impersonate="firefox_135", proxy="http://...")。相同指纹的请求会复用缓存的连接池(见 请求方法)。


流式响应

默认情况下响应体会被一次性读入内存。对于大文件下载分块/SSE 流式响应,传入 stream=True 即可增量读取——此时返回的是 StreamResponse,连接保持打开直到读完或 close()

同步(Client

with never_primp.Client(impersonate="chrome_147") as client:
    r = client.get("https://example.com/large-file.bin", stream=True)
    print(r.status_code, r.headers)        # 头部立即可用,body 尚未读取

    with open("large-file.bin", "wb") as f:
        # chunk_size=None:产出网络原生分片;指定整数:产出定长分片(末块可能更小)
        for chunk in r.iter_bytes(chunk_size=65536):
            f.write(chunk)

    # 或一次性读取剩余全部
    # data = r.read()

异步(AsyncClient

async with never_primp.AsyncClient(impersonate="chrome_147") as client:
    r = await client.get("https://example.com/stream", stream=True)
    async for chunk in r.aiter_bytes():    # 逐块异步消费
        process(chunk)
    # 或:body = await r.aread()

StreamResponse 接口:

成员 说明
status_code / url / headers / cookies / encoding Response 一致,发送后立即可用
iter_bytes(chunk_size=None) 同步迭代器;for chunk in r.iter_bytes(): ...
aiter_bytes(chunk_size=None) 异步迭代器;async for chunk in r.aiter_bytes(): ...
read() / await aread() 读取并返回剩余全部 body 字节
close() 提前关闭流,释放底层连接(不再使用时也可直接丢弃对象)

高并发下请尽快消费流(连接在读完前一直占用),或对不需要的响应调用 close()


异常处理

请求执行期间的失败会抛出类型化异常,便于按错误类型决定重试 / 切换代理,无需匹配错误字符串:

HTTPError                 # 基类,可一把捕获所有请求错误
├── ConnectionError       # DNS / 连接 / TLS 握手 / 代理 / 连接重置 / 请求未发出
├── TimeoutError          # 请求或读取超时
├── StatusError           # error_for_status 触发的 4xx / 5xx
└── DecodeError           # body 读取或解码失败
import never_primp
from never_primp import HTTPError, ConnectionError, TimeoutError

client = never_primp.Client(impersonate="chrome_147", timeout=10.0)

def fetch_with_retry(url, proxies, retries=3):
    for attempt in range(retries):
        try:
            return client.get(url, proxy=proxies.current)
        except TimeoutError:
            continue                       # 超时:直接重试
        except ConnectionError:
            proxies.rotate()               # 连接/代理故障:换代理再试
        except HTTPError:
            raise                          # 其它请求错误:交给上层
    raise RuntimeError("重试次数耗尽")

这些异常类挂在包命名空间下(never_primp.TimeoutError 等),是 Python 内建同名异常的独立类型。配置类错误(如无效的浏览器名、非法 URL)仍抛 RuntimeError / ValueError


API 参考

Client 构造参数

client = never_primp.Client(
    # 认证
    auth=("username", "password"),  # Basic Auth
    auth_bearer="token",            # Bearer Auth

    # 网络
    proxy="socks5://127.0.0.1:1080",
    timeout=30.0,                   # 总超时(秒)
    verify=True,                    # SSL 证书验证
    ca_cert_file="/path/to/ca.pem", # 自定义 CA 证书

    # 浏览器伪装
    impersonate="chrome_147",
    impersonate_os="windows",       # windows/macos/linux/android/ios

    # HTTP 协议
    http1_only=False,               # 强制 HTTP/1.1
    http2_only=False,               # 强制 HTTP/2
    https_only=False,               # 拒绝 HTTP 请求
    follow_redirects=True,
    max_redirects=20,

    # Cookie
    # cookie_store=True 时,所有 Set-Cookie 响应头被持久化并自动发送
    # 大并发无状态抓取建议设为 False,显著降低内存和锁争用
    cookie_store=True,
    split_cookies=True,             # HTTP/2 风格 Cookie 头

    # 重试
    max_retries=2,                  # 网络错误时的最大重试次数

    # 连接池(可选,有合理默认值)
    pool_max_idle_per_host=32,      # 每 host 最多保留的空闲连接数(默认 32)
    pool_max_size=512,              # 全局连接总上限(默认 512)
    pool_idle_timeout=30.0,         # 空闲连接关闭前的等待秒数(默认 30)

    # 默认值(所有请求共享)
    headers={"X-Custom": "value"},
    params={"version": "2"},
)

请求方法

# 所有方法支持相同的参数集
r = client.get(url,
    params={"q": "python"},
    headers={"Accept": "application/json"},
    cookies={"session": "abc"},
    timeout=10.0,
    proxy="http://127.0.0.1:8080",
    verify=False,
    # 请求级别临时覆盖
    impersonate="firefox_147",
    impersonate_os="linux",
)

r = client.post(url,
    json={"key": "value"},            # JSON 请求体
    # data={"key": "value"},          # Form 表单
    # content=b"raw bytes",           # 原始字节
    # files={"file": "/path/to/file"},# 文件上传(multipart)
)

# stream=True 返回 StreamResponse,增量读取 body(见“流式响应”)
r = client.get(url, stream=True)

请求级指纹覆盖会被缓存复用:当某次请求带了 impersonate / verify / http1_only 等覆盖参数时,会按这组参数构建并缓存一个独立的客户端(含连接池)。后续相同指纹的请求复用同一缓存客户端,从而保留 TCP/TLS 连接复用——轮换指纹不再每次重建客户端。proxy / timeout 不参与缓存键(在请求级单独应用),因此代理轮换始终是廉价的。close() 会清空该缓存。

Response 对象

r = client.get("https://httpbin.org/get")

r.status_code      # int: 200
r.url              # str: 最终 URL(重定向后)
r.headers          # dict[str, str]: 响应头
r.cookies          # dict[str, str]: Set-Cookie 解析结果
r.content          # bytes: 原始响应体
r.text             # str: 自动编码检测后的文本
r.encoding         # str: 检测到的编码
r.json()           # Any: JSON 解析

Cookie 管理(通过 client.cookies

client.cookies 返回 RequestsCookieJar,提供以下接口:

jar = client.cookies  # RequestsCookieJar

# ── 类 dict 接口(全局 Cookie)──────────────────────────────────
jar["name"]                        # → str,获取
jar["name"] = "value"              # 设置
del jar["name"]                    # 删除
"name" in jar                      # → bool
len(jar)                           # → int
iter(jar)                          # 遍历所有 name

jar.get(name, default=None)        # → str | None
jar.update({"k": "v"})            # 批量设置全局 Cookie
jar.keys()                         # → list[str]
jar.values()                       # → list[str]
jar.items()                        # → list[tuple[str, str]]
jar.get_dict()                     # → dict[str, str],全部 Cookie 序列化

# ── 域名 Cookie ─────────────────────────────────────────────────
jar.set(name, value,               # 设置域名 Cookie
        domain=None, path=None)
jar.get_jar_cookies()              # → list[tuple[name, value, domain, path]]
jar.get_cookies_for_url(url)       # → dict[str, str],RFC 6265 匹配
jar.find(name, default=None)       # → str | None,跨域名/路径查找

# ── 清空 ─────────────────────────────────────────────────────────
jar.clear_global()                 # 清空全局 Cookie
jar.clear_jar()                    # 清空域名 Cookie
jar.clear()                        # 清空全部

# ── 跨会话持久化(在 Client 上)────────────────────────────────
client.export_cookies()            # → list[tuple[name, value, domain, path]]
client.import_cookies([(name, value, domain, path)])  # → None

请求头管理方法

client.headers = {"User-Agent": "bot"}       # 设置(替换全部)
client.headers                               # 读取
client.set_header("X-Custom", "value")       # 设置单个
client.get_header("X-Custom")               # 读取单个 → str | None
client.headers_update({"Accept": "*/*"})     # 合并更新
client.delete_header("X-Custom")             # 删除单个
client.clear_headers()                       # 清空全部

会话关闭

# 方式一:上下文管理器(推荐)
# __exit__ / __aexit__ 会自动调用 close(),立即释放空闲 TCP 连接
with Client(impersonate="chrome_147", cookie_store=False) as client:
    r = client.get("https://example.com")

# 方式二:手动关闭
client = Client(impersonate="chrome_147")
# ... 若干请求 ...
client.close()   # 释放连接池;正在进行中的请求不受影响

# 注意:CPython 的引用计数机制保证 Client 对象离开作用域时
# 即时触发 Drop,连接池会自然释放。close() 的价值在于:
# 1. 明确语义(对熟悉 requests.Session.close() 的用户)
# 2. 处理全局变量或引用循环持有 Client 的边界情况
# 3. 使 with 语句具有实际清理效果(而不是空操作)

便利函数(无需创建 Client)

import never_primp

r = never_primp.get("https://httpbin.org/get")
r = never_primp.post("https://httpbin.org/post", json={"a": 1})
r = never_primp.put(url, data={"k": "v"})
r = never_primp.delete(url)
r = never_primp.patch(url, json={})
r = never_primp.head(url)
r = never_primp.options(url)

AsyncClient(异步接口)

构造参数与 Client 完全相同;所有请求方法为 async,需 await。这是真异步(非线程池),适合 asyncio.gather 大并发,并支持 stream=True。详见 异步流式响应

import asyncio
import never_primp

async def main():
    async with never_primp.AsyncClient(
        impersonate="chrome_147",
        max_retries=2,
    ) as client:
        r = await client.get("https://httpbin.org/get")
        print(r.json())

        # 大并发
        rs = await asyncio.gather(*(client.get(u) for u in urls))

        # 流式
        s = await client.get(big_url, stream=True)
        async for chunk in s.aiter_bytes():
            ...

asyncio.run(main())

开发

# 环境依赖
pip install maturin

# 开发构建(快速迭代)
maturin develop

# 发布构建(完整优化)
maturin develop --release

# 代码检查
cargo check
cargo clippy
cargo fmt

# 运行示例
python example/concurrent_requests.py
python example/cookie_management.py
python example/browser_impersonation.py

架构概览

Python 调用  client.get(url)              AsyncClient:  await client.get(url)
      ↓                                          ↓
Client.__init__.py   # Python 封装层(ergonomics)
      ↓                                          ↓
RClient.request()                        RClient.request_async()   # src/client.rs
      ↓                                          ↓
build_request_future()  # 共享:GIL 内完成所有准备,产出 Send future
      ↓                                          ↓
py.detach()+runtime().block_on()         future_into_py()  # 桥接到 asyncio
      ↓ (释放 GIL)                              ↓ (事件循环驱动)
            └──────────────┬───────────────────┘
                           ↓
              共享 Tokio 运行时(4 workers)
                           ↓
wreq::Client        # Rust HTTP 客户端(内部 Arc,无锁 clone)
                           ↓
wreq 连接池          # TCP 复用 + TLS 会话复用(含请求级指纹覆盖的缓存客户端)
                           ↓
目标服务器
                           ↓
finish_request()  # 缓冲→Response 或 stream=True→StreamResponse(src/response.rs)
                           ↓
返回 Python,GIL 重新获取

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

never_primp-3.0.1.tar.gz (988.6 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

never_primp-3.0.1-cp39-abi3-win_amd64.whl (3.3 MB view details)

Uploaded CPython 3.9+Windows x86-64

never_primp-3.0.1-cp39-abi3-win32.whl (3.0 MB view details)

Uploaded CPython 3.9+Windows x86

never_primp-3.0.1-cp39-abi3-musllinux_1_2_x86_64.whl (10.1 MB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ x86-64

never_primp-3.0.1-cp39-abi3-musllinux_1_2_aarch64.whl (9.7 MB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ ARM64

never_primp-3.0.1-cp39-abi3-manylinux_2_28_x86_64.whl (3.4 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.28+ x86-64

never_primp-3.0.1-cp39-abi3-manylinux_2_28_aarch64.whl (3.3 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.28+ ARM64

never_primp-3.0.1-cp39-abi3-macosx_11_0_arm64.whl (3.0 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

never_primp-3.0.1-cp39-abi3-macosx_10_12_x86_64.whl (3.2 MB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file never_primp-3.0.1.tar.gz.

File metadata

  • Download URL: never_primp-3.0.1.tar.gz
  • Upload date:
  • Size: 988.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.13.3

File hashes

Hashes for never_primp-3.0.1.tar.gz
Algorithm Hash digest
SHA256 02204ebfb619c15b718b84bd1ab1b823cd6b29756e5c5eafcc1932b95f8f3337
MD5 b250f948196c210167d46279d3150126
BLAKE2b-256 5aa1fd0e9fe5ec24e639cae034d48325711a445531de42a0d779a76755d82d84

See more details on using hashes here.

File details

Details for the file never_primp-3.0.1-cp39-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for never_primp-3.0.1-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 126dfa8a3c18bb7bf07f2fce948cb47290b2a4b421845e5ce68be5b1f62013ae
MD5 abafbbd5d85396dbe7dfc7221a8fcc62
BLAKE2b-256 8102d45549863320afbf0446e080e6824bd478fa444970a6910c6640dcd4fb10

See more details on using hashes here.

File details

Details for the file never_primp-3.0.1-cp39-abi3-win32.whl.

File metadata

  • Download URL: never_primp-3.0.1-cp39-abi3-win32.whl
  • Upload date:
  • Size: 3.0 MB
  • Tags: CPython 3.9+, Windows x86
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.13.3

File hashes

Hashes for never_primp-3.0.1-cp39-abi3-win32.whl
Algorithm Hash digest
SHA256 ecbc64d4f253f2665124d54cfbf7c67d6a9bc6d1b02a86d70cd02010002bbb51
MD5 f9590614555f52a8baa465ca9ea083ed
BLAKE2b-256 a621fe40ee7c29131a27ffd60fa5814378accb9d1afadbace76973775797caca

See more details on using hashes here.

File details

Details for the file never_primp-3.0.1-cp39-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for never_primp-3.0.1-cp39-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 573fae8b60e190f3e142413f7c2aab7358f0fdbc94f72280fdda374f48c5747b
MD5 20d0f133e8b57759c592548e73bc1c40
BLAKE2b-256 dd222dc1738d4604e929e3e794283acd57314495f68ad4b3def8df3065c36e84

See more details on using hashes here.

File details

Details for the file never_primp-3.0.1-cp39-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for never_primp-3.0.1-cp39-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 5d0b69d4b37b9862a4d048b2c4963f30cebf69392147c9ac591fbe637b78f02b
MD5 fcaf62550f8de32a6d8c95c93ed73ce6
BLAKE2b-256 17d81ea60b1d054f61d28720b96aa9a76d40472bbb95bf950e44b90bf29d2f33

See more details on using hashes here.

File details

Details for the file never_primp-3.0.1-cp39-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for never_primp-3.0.1-cp39-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 71f6d4a195d7833f863fadf528ca63be7013066fa010a4964f04cd2ecc3c6747
MD5 db3b8c85a8f1e560418ce07fce953210
BLAKE2b-256 addae29a87b9e923c4bc08d3b0b7aad667b8182a333dcc5e75460c2ff7556ab0

See more details on using hashes here.

File details

Details for the file never_primp-3.0.1-cp39-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for never_primp-3.0.1-cp39-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 359ff7dc61a874de5d82ee2f19d0f75742d9eb4e192f64083cb1ec5ab174b85d
MD5 46ce6effb0adee13abe64c53adeaf084
BLAKE2b-256 1953eee9b4830bb7bd7ad7683af156c783faf0ab20559f6695b48deb83e10707

See more details on using hashes here.

File details

Details for the file never_primp-3.0.1-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for never_primp-3.0.1-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a1b2be73f72d42fd0c87c6bfbc360e33c94ad9e0c2ddd1db6f5fc97459aa5294
MD5 74621fdc128e69acf44288cb94778a4c
BLAKE2b-256 517d4077484217b4fc13c010ade6e4f43e0f80e03a5e98131dc4b42f6eddca2b

See more details on using hashes here.

File details

Details for the file never_primp-3.0.1-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for never_primp-3.0.1-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 3a2ea3cdc587108d76f853afd967287b15dc64fa655eaa53fff63ab4b541893f
MD5 b4f906ebd78068f9ef4cb4a27cd6e3b9
BLAKE2b-256 6637104b3dd8d72e21a49f89c4cbb9bccfd933fbba1e1ab5ad69922d956a6502

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page