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 错误的比例,相比于便宜的数据中心池。