在爬虫、数据采集和多店铺运营中,代理 IP 是绕过目标站频率限制和地理封锁的常用手段。但直接给requests.get()加一个proxies参数只是第一步,真正影响稳定性和效率的是代理轮换、Session 复用、超时重试、并发控制和请求指纹。
本文整理一套可直接落地的 Python 代理接入与网络优化方案,适用于 requests、Scrapy 和异步爬虫。
一、最小接入:requests 如何正确配置代理
1.1 基础写法
import requests proxies = { "http": "http://user:password@proxy.example.com:8080", "https": "http://user:password@proxy.example.com:8080", } resp = requests.get( "https://target-site.com/api", proxies=proxies, timeout=(5, 20), # (connect_timeout, read_timeout) headers={"User-Agent": "Mozilla/5.0"}, ) print(resp.status_code)注意:访问https://目标时,代理地址通常仍写成http://(代理通过 CONNECT 建立隧道)。如果代理本身支持 HTTPS 入口,才写成https://。
1.2 特殊字符密码
如果密码包含@、:、#等特殊字符,直接拼 URL 会解析出错。建议用urllib.parse.quote编码:
from urllib.parse import quote user = "myuser" password = "pa@ss:123" proxy_url = f"http://{user}:{quote(password, safe='')}@proxy.example.com:8080"二、Session 复用:减少握手开销
每次请求都新建 TCP/TLS 连接会显著增加延迟。使用requests.Session()可以复用连接池:
import requests from requests.adapters import HTTPAdapter session = requests.Session() session.proxies = { "http": "http://user:password@proxy.example.com:8080", "https": "http://user:password@proxy.example.com:8080", } # 限制连接池大小和重试次数 session.mount("http://", HTTPAdapter(pool_connections=10, pool_maxsize=20)) session.mount("https://", HTTPAdapter(pool_connections=10, pool_maxsize=20)) for i in range(100): try: resp = session.get( f"https://target-site.com/items/{i}", timeout=(5, 20), headers={"User-Agent": "Mozilla/5.0"}, ) print(i, resp.status_code) except requests.exceptions.RequestException as e: print(i, type(e).__name__, e)对于代理池,建议为每个代理地址维护一个 Session,避免不同认证/出口混在一起导致连接复用失败。
三、代理轮换:避免单节点被封
3.1 简单轮询
from itertools import cycle import requests proxy_list = [ "http://user:pass@proxy-a.example.com:8080", "http://user:pass@proxy-b.example.com:8080", "http://user:pass@proxy-c.example.com:8080", ] proxy_iter = cycle(proxy_list) for i in range(100): proxy = next(proxy_iter) try: resp = requests.get( "https://target-site.com/api", proxies={"http": proxy, "https": proxy}, timeout=(5, 20), ) except requests.exceptions.RequestException: continue3.2 按失败率动态剔除
更稳健的做法是记录每个代理的成功率和延迟,优先使用表现好的节点。核心思路:维护一个ProxyStats对象,每次请求后更新total/ok/latencies,选择时按success_rate和p95综合评分排序,避免一直用已经失败的节点。
from dataclasses import dataclass, field from collections import deque @dataclass class ProxyStats: url: str total: int = 0 ok: int = 0 latencies: deque = field(default_factory=lambda: deque(maxlen=50)) @property def success_rate(self) -> float: return self.ok / self.total if self.total else 0.0选择时按success_rate * 0.7 + (1 / p95) * 0.3综合评分排序即可。
四、超时与重试:不要把所有失败都重试
不是所有异常都值得重试。建议只重试连接超时、读取超时和 5xx,403/407/429 应先分析原因:
from functools import wraps import requests def fetch_with_retry(session, url, max_retries=3, backoff=1.0): for attempt in range(max_retries + 1): try: resp = session.get(url, timeout=(5, 20)) if resp.status_code >= 500: if attempt == max_retries: return resp time.sleep(backoff * (2 ** attempt)) continue return resp except (requests.exceptions.ConnectTimeout, requests.exceptions.ReadTimeout) as e: if attempt == max_retries: raise time.sleep(backoff * (2 ** attempt)) except (requests.exceptions.ProxyError, requests.exceptions.SSLError) as e: # 代理认证或 TLS 错误,重试通常无效 raise注意:重试前最好换代理。同一节点重复请求 429,会加速被封。
五、并发控制:aiohttp 与异步请求
对于 I/O 密集型爬虫,异步能显著提升吞吐:
import asyncio import aiohttp async def fetch(session, url, proxy): async with session.get(url, proxy=proxy, timeout=aiohttp.ClientTimeout(total=20)) as resp: return await resp.text(), resp.status async def main(): urls = [f"https://target-site.com/items/{i}" for i in range(100)] proxy = "http://user:password@proxy.example.com:8080" connector = aiohttp.TCPConnector(limit=20, limit_per_host=5) async with aiohttp.ClientSession(connector=connector) as session: tasks = [fetch(session, url, proxy) for url in urls] results = await asyncio.gather(*tasks, return_exceptions=True) return results asyncio.run(main())关键点:limit控制全局连接数,limit_per_host控制单目标站连接数,避免触发目标站限流。并发不是越大越好,先压测到稳定阈值;异步场景下也要做代理轮换和失败统计。
六、请求指纹:降低被识别概率
代理只是绕过 IP 限制的一部分。如果请求头、Cookie、TLS 指纹或行为模式一致,仍然容易被识别。建议:
| 维度 | 建议 |
|---|---|
| User-Agent | 准备 5–10 个常见浏览器 UA,按请求或会话轮换 |
| Accept/Language | 与 UA 所在地区匹配 |
| Cookie/Session | 不同账号/会话使用不同 Cookie 池 |
| 请求间隔 | 加入随机抖动,如time.sleep(random.uniform(0.5, 2.5)) |
| 并发节奏 | 避免短时间内集中请求同一接口 |
| 请求头顺序 | 尽量贴近真实浏览器,必要时使用真实浏览器驱动 |
七、日志与监控:把问题拆清楚
建议在日志中记录以下字段:
| 字段 | 用途 |
|---|---|
target_url | 判断是否只有某个目标失败 |
proxy_url | 定位问题节点 |
status_code | 区分 403/407/429/5xx |
exception_type | 区分 ConnectTimeout / ReadTimeout / ProxyError |
latency_ms | 判断延迟是否异常 |
retry_count | 评估重试策略是否有效 |
body_preview | 快速判断返回的是正常页还是校验页 |
一个最小日志模板:
import logging import time logger = logging.getLogger("crawler") def log_request(url, proxy, start, resp=None, exc=None): latency = (time.perf_counter() - start) * 1000 extra = {"target": url, "proxy": proxy, "latency_ms": round(latency, 2)} if exc: extra["exception"] = type(exc).__name__ logger.warning("req_fail", extra=extra) else: extra["status"] = resp.status_code logger.info("req_ok", extra=extra)八、常见坑
- 代理地址协议写错:访问 HTTPS 目标时,代理地址通常仍是
http://,不是https://。 - 密码特殊字符未编码:含
@、:的密码不编码会导致 URL 解析失败。 - 不复用 Session:每个请求新建连接会显著增加 P95 延迟。
- 重试 403/429:直接重试会加速被封,应先降速或换代理。
- 只换 IP 不换指纹:UA、Cookie、请求节奏一致仍会被识别。
- 并发过高:代理网关和节点都有容量上限,超过后成功率会断崖式下降。
九、总结
Python 爬虫接入代理 IP,核心不是"能不能挂上代理",而是能否把代理稳定地放进工程闭环:
- Session 复用降低握手开销;
- 代理轮换 + 失败统计避免单节点失效拖垮整体;
- 分层超时 + 选择性重试减少无效请求;
- 异步并发控制提升吞吐但不突破代理容量;
- 请求指纹管理降低被识别概率;
- 结构化日志让问题可追踪。
把这些环节串起来,才能把代理从"能跑"变成"稳定跑、可监控、可优化"。如果你使用 IPdodo 的跨境专线或静态住宅 IP,可以把同样的接入逻辑套用到其提供的入口上,结合自身业务目标站做压测和调优。
参考资料
- Requests 官方文档:Proxies
- aiohttp Client Reference
- Scrapy Downloader Middleware
- Python urllib.parse.quote