← 返回博客

Python中的httpx异步代理:轮换、HTTP/2及新手常见的5个错误

我们将讨论如何正确设置httpx中的代理轮换,启用HTTP/2,并避免90%初学者解析器开发者常见的错误。

📅2026年10月17日

httpx 库已成为 Python 中异步 HTTP 请求的标准,但在使用代理时,它有一些细节会在生产环境中破坏解析器。我们将讨论如何配置异步代理轮换,启用 HTTP/2,以及为什么您的脚本在每 50-100 次请求时崩溃。

为什么选择 httpx,而不是 requests 或 aiohttp

requests 是同步的,无法开箱即用地支持 HTTP/2。aiohttp 是异步的,但其代理设置不够友好,并且完全不支持 HTTP/2。httpx 解决了这两个问题:提供同步和异步客户端的统一 API,原生支持通过 h2 扩展的 HTTP/2,以及在传输层灵活处理代理。

对于市场(Wildberries、Ozon、Avito)解析、社交媒体 API 数据收集或竞争对手价格监控等任务,异步性至关重要——它提供了与顺序请求相比的显著速度提升。但如果没有合理的代理轮换和连接管理,异步性会变成新问题的来源:连接池耗尽、协程挂起、文件描述符泄漏。

第二个重要点是 HTTP/2 可以在一个 TCP 连接中复用多个请求,这减少了对目标服务器的负担,并有助于绕过一些简单的反机器人系统,这些系统会查看来自同一 IP 的 TCP 握手数量。

httpx 中代理的基本设置

从 httpx 0.26+ 版本开始,代理的设置语法发生了变化——不再使用字典 proxies,而是使用参数 proxy。如果您查看旧教程,这可能是一个常见的混淆来源。

import httpx
import asyncio

PROXY_URL = "http://user:[email protected]:8000"

async def fetch(url: str):
    async with httpx.AsyncClient(proxy=PROXY_URL, timeout=15.0) as client:
        response = await client.get(url)
        return response.status_code, response.text[:200]

asyncio.run(fetch("https://httpbin.org/ip"))

如果需要为 HTTP 和 HTTPS 流量分开代理,或为不同域名设置不同的代理,请使用 httpx.Client(mounts=...)——传输会根据 URL 模式进行挂载:

mounts = {
    "http://": httpx.AsyncHTTPTransport(proxy="http://user:[email protected]:8000"),
    "https://": httpx.AsyncHTTPTransport(proxy="http://user:[email protected]:8000"),
}

async with httpx.AsyncClient(mounts=mounts) as client:
    response = await client.get("https://example.com")

对于用户名和密码的身份验证,格式 http://user:pass@host:port 在 HTTP 代理中稳定工作。对于 SOCKS5,您需要额外安装 httpx[socks],否则您将在运行时遇到 ImportError,而不是在编写代码时——这是许多人会陷入的陷阱。

异步代理轮换:可用代码

代理轮换中的主要错误是为每个请求创建新的 AsyncClient。这会降低性能,因为客户端保持连接池,重新创建它会抵消保持连接的所有优势。正确的方法是在预先创建客户端池,每个代理一个,并在它们之间分配任务。

import httpx
import asyncio
import itertools
import random

PROXIES = [
    "http://user:[email protected]:8000",
    "http://user:[email protected]:8000",
    "http://user:[email protected]:8000",
]

class ProxyPool:
    def __init__(self, proxies: list[str]):
        self.clients = [
            httpx.AsyncClient(proxy=p, timeout=httpx.Timeout(15.0, connect=5.0))
            for p in proxies
        ]
        self._cycle = itertools.cycle(self.clients)

    def get_client(self) -> httpx.AsyncClient:
        return next(self._cycle)  # 循环选择
        # 随机选择:random.choice(self.clients)

    async def close_all(self):
        for client in self.clients:
            await client.aclose()

async def fetch_with_retry(pool: ProxyPool, url: str, retries: int = 3):
    for attempt in range(retries):
        client = pool.get_client()
        try:
            response = await client.get(url)
            if response.status_code == 200:
                return response.text
        except (httpx.ConnectTimeout, httpx.ProxyError, httpx.ReadTimeout):
            await asyncio.sleep(0.5 * (attempt + 1))
            continue
    return None

async def main():
    pool = ProxyPool(PROXIES)
    urls = [f"https://example.com/page/{i}" for i in range(100)]

    sem = asyncio.Semaphore(20)  # 限制并发

    async def bound_fetch(url):
        async with sem:
            return await fetch_with_retry(pool, url)

    results = await asyncio.gather(*(bound_fetch(u) for u in urls))
    await pool.close_all()
    return results

asyncio.run(main())

这个模式同时解决了三个问题:通过保持连接重用 TCP 连接,按循环方式分配负载到代理,并通过信号量限制并发,以免压垮目标服务器并引发速率限制封禁。

对于与 Wildberries、Ozon 或 Instagram API 交互的生产解析器,确保代理池足够大且地理位置多样化非常重要——否则在同一子网内的轮换无法避免封禁。在这里,住宅代理 是一个不错的选择,因为它们拥有真实用户的 IP,较少被反机器人系统过滤,这些系统分析 ASN 和 IP 范围的声誉。

httpx 中的 HTTP/2:为什么以及如何启用

默认情况下,httpx 使用 HTTP/1.1。要启用 HTTP/2,您需要安装扩展并明确传递标志:

# pip install httpx[http2]

async with httpx.AsyncClient(http2=True, proxy=PROXY_URL) as client:
    response = await client.get("https://example.com")
    print(response.http_version)  # 期望 "HTTP/2"

一个重要的细节是,并非所有代理都能正确支持 HTTP/2 隧道。一些便宜的数据中心代理即使目标服务器支持 HTTP/2,仍然只能作为 HTTP/1.1 代理工作——在这种情况下,httpx 会悄悄降级到通过 CONNECT 隧道的 HTTP/1.1,您并不总是能在没有明确检查 response.http_version 的情况下注意到这一点。

HTTP/2 在三种场景中提供了实际的优势:当您对同一域名发出大量请求时(在一个连接中复用减少延迟),当降低请求对反机器人保护的可见性很重要时(来自同一 IP 的 TCP 握手更少看起来更自然),以及当目标网站本身要求 HTTP/2 并向 HTTP/1.1 客户端返回错误或削减内容时——这在使用 Cloudflare 的网站上偶尔会出现。

对于需要通过代理实现高速和稳定的 HTTP/2 连接的任务,专用的 数据中心代理 更为合适——它们比住宅代理更快,通常能正确支持现代协议的隧道。

httpx 中与代理相关的 5 个常见错误

1. 每个请求都重新创建客户端

代码形式 async with httpx.AsyncClient(...) as client: await client.get(url) 在循环内部——这是反模式。每次创建客户端都会建立新的 TCP 连接,新的 TLS 握手,完全忽视连接池。在 1000 次请求中,正确和错误方法之间的速度差异可能达到 5-10 倍。

2. 没有超时或默认超时过长

默认情况下,httpx 对所有请求设置 5 秒的超时——连接、读取、写入、池。在通过不稳定的代理工作时,这对连接来说太少,但对读取来说太多,如果代理只是“挂起”。明确分开超时:

timeout = httpx.Timeout(
    connect=5.0,
    read=10.0,
    write=10.0,
    pool=5.0
)
client = httpx.AsyncClient(proxy=PROXY_URL, timeout=timeout)

3. 忽视连接池的限制

在大量并行任务(在没有信号量的情况下 gather 500+ 协程)时,httpx 会开始抛出 PoolTimeout,因为连接池中的连接限制(默认 100)已用尽。通过 httpx.Limits(max_connections=200, max_keepalive_connections=50) 明确设置限制,并始终使用信号量来控制并发。

4. 在不理解后果的情况下禁用验证

许多人从 Stack Overflow 复制 verify=False 以消除通过带有 MITM 证书的代理时的 SSL 错误。这为“中间人”攻击打开了大门,并可能导致通过被攻陷的代理泄露数据。如果代理需要特定的证书——请通过 verify="/path/to/cert.pem" 明确添加,而不是完全禁用验证。

5. 客户端未关闭导致文件描述符泄漏

如果您手动创建客户端池(如上面的示例),请记得在完成工作时为每个客户端调用 await client.aclose()。长时间运行的脚本如果没有明确关闭连接,会达到操作系统打开文件描述符的限制(通常在 Linux 中默认为 1024),解析器会在运行几个小时后因神秘错误 Too many open files 而崩溃。

选择哪种类型的代理进行解析

代理的选择直接影响您会看到多少上述错误——不稳定的代理会导致超时和连接中断,即使代码编写得再完美。

代理类型 速度 封禁风险 何时使用
数据中心 高 在受保护的网站上高 开放 API,反机器人保护弱
住宅 中等 低 Wildberries、Ozon、社交媒体
移动 中低 最低 Instagram、TikTok API、严格保护

对于异步解析,快速请求 API 的情况下,数据中心代理仍然是经济实惠的选择——关键是均匀分配负载,如上面的代码所示。

生产前检查清单

  • httpx 客户端一次创建并重用,而不是每次请求都创建
  • 超时分为 connect/read/write/pool,而不是使用默认值
  • 通过 asyncio.Semaphore 限制并发
  • 连接池的限制根据实际负载进行设置
  • 有重试逻辑处理 ProxyError、ConnectTimeout、ReadTimeout
  • 所有客户端在结束时通过 aclose() 明确关闭
  • 在没有极端必要的情况下不使用 verify=False
  • HTTP/2 有意识地启用并通过 response.http_version 进行检查

结论

httpx 提供了强大的工具来异步处理代理和 HTTP/2,但需要仔细配置客户端池、超时和连接限制。大多数解析器在生产环境中的崩溃与库无关,而是与架构错误有关:重新创建客户端、缺乏并发限制和不当处理代理异常。

如果您正在构建针对市场或社交媒体 API 的解析器,这些 API 会积极监控可疑活动,代理基础设施的稳定性与代码同样重要。对于这些任务,建议考虑 住宅代理——它们降低了由于 IP 地址声誉较高而导致的 ProxyError 和 ConnectTimeout 错误的比例,相比于便宜的数据中心池。