移动代理 Python 爬虫
适用于请求、httpx、aiohttp 和 Scrapy 的可运行代理代码——人们常误解的代理字典、在实际网站中经得起考验的轮询模式,以及能遵守 Retry-After 指示而非不断触发 429 错误的退避策略。
- 支持标准库中的请求库——requests、httpx、aiohttp 和 Scrapy 无需使用特殊的客户端。
- 一个代理 URL — 所有客户端都能识别的统一字符串:http://user:pass@host:port。
- 按需轮换 — 在任务边界处通过您自己的代码触发新的 IP 地址。
- 无限带宽 —— 无论并发量和数据包大小如何,账单金额均保持不变。
普通的 HTTP(S) 和 SOCKS5 端点——无需集成供应商的 SDK。
在任务边界处触发新的退出 IP,而不是根据固定的定时器触发。
请求与 httpx:会话、代理字典和 CONNECT 隧道
如果所需的标记信息已包含在初始响应中,请勿启动浏览器。HTTP 客户端在内存和时间消耗方面要低一个数量级。使用单个会话,以便在不同请求之间复用连接池和 Cookie 存储,而不是每次调用都重新协商 TLS:
import os
import requests
PROXY = os.environ["PXM2_PROXY"] # http://user:pass@host:port
with requests.Session() as s:
s.proxies.update({"http": PROXY, "https": PROXY})
s.headers.update({
"User-Agent": "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 ...",
"Accept-Language": "en-GB,en;q=0.9",
})
r = s.get("https://example.com/listings", timeout=30)
r.raise_for_status()
人们常搞错的一点是 https 条目。它仍然使用 http:// 方案,因为该方案描述的是如何连接到代理——而非目标站点的协议。客户端通过代理打开一个 CONNECT 隧道,随后 TLS 在该隧道内进行端到端传输,因此代理永远看不到你的明文。 在那里写成 https:// 才是 Python 代理在 HTTPS 环境下看似无法工作的最常见原因。
第二个要点是一致性:移动设备的User-Agent应与移动IP地址相匹配。从数据中心IP段声称自己是安卓手机,这种矛盾任何指纹识别层都能察觉。
httpx 的工作原理相同,只是关键点不同——将代理信息传递给 Client 构造函数。当您需要使用 HTTP/2 时,建议优先选择这种方式,因为采用类似浏览器的协议版本,可以减少请求头声明与客户端行为之间的不一致之处。
| 图书馆 | 代理的去向 | 旋转挂钩 | 最适合 |
|---|---|---|---|
| requests | Session.proxies 字典,或按调用设置的代理= | 每个代理仅限一次会话 | 简单的同步爬行 |
| httpx | proxy= 客户端 | 每个代理仅限一个客户端 | HTTP/2,即在单一 API 中兼顾同步与异步 |
| aiohttp | proxy= 位于单个请求中 | 应要求,因此旋转操作非常简单 | 高并发 |
| Scrapy | request.meta["proxy"] | 一个下载器中间件 | 大规模结构化爬取 |
Python 中的代理轮询:循环轮询、随机轮询和故障轮询
这里有两种截然不同的情况,将它们混为一谈是造成大部分困惑的原因。如果你只被分配了一个会轮换的端点,那么轮换是在上游进行的,你的代码只需决定何时请求新的 IP 地址。如果你持有一组端点,那么轮换策略就由你自己决定。
对于第二种情况,应为每个会话分配一个代理,而不是为每个请求分配一个。会话承载着连接池和Cookie池,因此在会话进行过程中更换其代理,就违背了设置代理的初衷:
import itertools
import requests
PROXIES = [
"http://user:pass@host:10001",
"http://user:pass@host:10002",
"http://user:pass@host:10003",
]
def sessions_for(proxies):
"One Session per exit IP, reused for the life of a job."
out = []
for p in proxies:
s = requests.Session()
s.proxies.update({"http": p, "https": p})
out.append(s)
return out
pool = itertools.cycle(sessions_for(PROXIES))
for job in jobs:
session = next(pool) # a whole job stays on one IP
run(session, job)
请注意该循环不会做什么:它在 run() 函数内部绝不会更改 IP 地址。它会在逻辑任务之间轮换,绝不会在某个任务进行到一半时切换——如果在经过身份验证的会话过程中退出 IP 地址发生变化,这看起来就像是会话劫持,其危害程度比数据抓取还要严重。
当任务相互独立且希望避免出现可预测的轮询周期时,随机选择是轮询调度的一种合理替代方案。“故障轮换”是一种值得添加到这两种调度方式中的模式:当代理开始返回阻塞时,应将其从池中移除,而不是继续向其分配任务。
处理 429 状态码:Retry-After、指数退避和抖动
429 状态码表明目标服务器提示速率错误。轮换 IP 地址并再次发送请求,会将速率问题误判为身份验证问题,而这正是爬虫从受限状态升级为被封锁状态的原因。请遵守 Retry-After 规则,并采用指数退避策略:
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(
total=5,
backoff_factor=1.5, # 1.5s, 3s, 6s, 12s, 24s
status_forcelist=[429, 500, 502, 503, 504],
respect_retry_after_header=True,
allowed_methods=["GET", "HEAD"],
)
s.mount("https://", HTTPAdapter(max_retries=retry, pool_maxsize=20))
如果正在运行并发 worker,请在请求间加入随机延迟,否则它们都会在同一瞬间醒来并重试——这会形成一股“雷鸣般的洪流”,其机械感远比原始流量更明显。而且并发性应按目标主机分别处理,而非全局统一:八个并行请求分散在八个域名上属于正常情况,而八个请求同时针对一个域名则属于突发流量。
重试操作必须是幂等的。status_forcelist 加上一个用于创建某项内容的 POST 请求,会毫无问题地多次创建该项。如上所述,将 allowed_methods 限制为 GET 和 HEAD 是安全的默认设置。
Scrapy 与 asyncio:中间件、aiohttp 及并发限制
Scrapy already ships proxy support. Its built-in HttpProxyMiddleware reads request.meta["proxy"], so setting that key in a spider or a small middleware is enough for a single endpoint. For rotation across a list, scrapy-rotating-proxies adds a pool and its own ban tracking:
ROTATING_PROXY_LIST = [
"http://user:pass@host:10001",
"http://user:pass@host:10002",
]
# or: ROTATING_PROXY_LIST_PATH = "proxies.txt"
DOWNLOADER_MIDDLEWARES = {
"rotating_proxies.middlewares.RotatingProxyMiddleware": 610,
"rotating_proxies.middlewares.BanDetectionMiddleware": 620,
}
CONCURRENT_REQUESTS_PER_DOMAIN = 4
DOWNLOAD_DELAY = 0.5
AUTOTHROTTLE_ENABLED = True
其默认的封禁启发式算法会将非200状态码、空正文或异常视为“死代理”,这种处理方式过于粗糙——如果目标服务器对封锁请求返回200状态码并显示挑战页面,该算法将直接放行该请求。 请通过一个能够识别特定目标在阻止您时行为的类,来覆盖 ROTATING_PROXY_BAN_POLICY。
在保持不被阻塞方面,底部的这两个设置比中间件更为重要。AutoThrottle 会根据检测到的延迟动态调整延迟时间,而按域设置的并发上限则能防止大规模爬取演变为对单个主机的突发请求。
使用 aiohttp 时,代理是针对单个请求而非会话设置的,这使得按请求轮换变得轻而易举,而按主机进行“礼貌”爬取则完全取决于你。请将你的请求封装在一个以目标主机为键的信号量中——因为 asyncio 会让“礼貌”爬取轻易演变成你未曾预料的拒绝服务攻击。
获取专用的 Python 爬虫代理
PXM2实时节点——选择您希望目标用户看到请求来自的国家/地区,即可获得一个专属的4G/5G IP地址,该地址提供无限带宽和轮询功能:
法国
印度
新加坡
常见问题解答
如何在 Python requests 中轮换代理?
维护一份端点列表,并使用 `itertools.cycle` 对其进行循环处理,为每个会话(Session)分配一个代理,而不是为每个请求分配——会话承载着连接池和 Cookie 存储,因此在会话进行中更换其代理将使设置会话的意义不复存在。 对于单个轮转端点,轮转操作在上游进行,您的代码只需决定何时请求新的 IP 地址即可。
为什么我的 Python 代理无法处理 HTTPS?
这几乎总是因为代理字典(proxies dict)中的 https 条目被赋予了 https:// 方案。该方案描述的是如何连接到代理,而非目标服务器的协议;对于普通的 HTTP 代理,该方案仍应为 http://。随后,客户端会通过该代理建立一个 CONNECT 隧道,而 TLS 则在该隧道内端到端运行。
如何在 aiohttp 中使用代理?
将 proxy= 参数传递给单个请求,而不是传递给会话,因为 aiohttp 会按每次调用处理该参数。 凭据应包含在 URL 中,或通过作为 `proxy_auth` 参数传递的 `aiohttp.BasicAuth` 对象提供。使用信号量限制每个目标主机的并发数——asyncio 使得将“礼貌爬取”转变为触发速率限制的突发请求变得轻而易举。
如何在 Scrapy 中设置代理?
内置的 HttpProxyMiddleware 会读取 request.meta["proxy"],因此只需在爬虫或自定义中间件中设置该键,即可处理单个端点。 若需在列表中轮换代理,scrapy-rotating-proxies 会将 ROTATING_PROXY_LIST 及其自身的中间件添加到轮换列表中,并记录哪些端点被封禁,从而停止使用这些端点。
在 Python 中进行网页抓取时,如何处理 429 错误?
读取 Retry-After 标头,并等待该时长。 如果服务器未发送该标头,则采用带抖动的指数退避策略。urllib3 的 Retry 模块同时支持这两种机制——设置 respect_retry_after_header 和 backoff_factor 参数,将其挂载到 HTTPAdapter 上,并克制立即轮换 IP 地址并重试的冲动: 429 错误是速率问题,若将其视为身份验证问题,则会导致爬虫操作升级为硬性封锁。
相关移动代理指南
如果您的目标通过 JavaScript 渲染数据,那么上面的 HTTP 客户端路径将无法访问它——浏览器指南中已涵盖这种情况。
网页抓取指南
核心移动代理指南
将您的 Python 爬虫指向运营商 IP
在普通的 HTTP(S) 和 SOCKS5 端点上配备专用 4G/5G 调制解调器——无需供应商 SDK,无带宽限制,且可通过您自己的代码触发 IP 轮换。
获取一个 Python 爬虫代理