速率限制是导致解析器崩溃、API集成中断以及自动化脚本收到429 Too Many Requests状态的最常见原因之一。服务器检测到来自同一IP的请求过多——然后就停止响应。在本文中,我们将讨论如何正确构建代理基础设施,以便在没有封禁和故障的情况下绕过请求限制——并提供Python和Node.js的实际代码示例。
什么是速率限制,为什么普通延迟无效
速率限制(请求频率限制)是一种服务器保护机制,限制来自单一来源在特定时间段内的请求数量。来源通常是IP地址,但高级系统还会考虑授权令牌、User-Agent、cookies甚至行为模式。
当您的脚本超过限制时,服务器会返回以下响应之一:
429 Too Many Requests— 速率限制的标准HTTP状态503 Service Unavailable— 有时用作429的替代403 Forbidden— 如果IP已被列入黑名单- 空响应或超时 — 在激进的封锁情况下
大多数开发者的第一反应是添加time.sleep(1)在请求之间。这仅在非常宽松的限制下有效(例如,每分钟60个请求)。但实际场景更复杂:
流行平台的实际限制:
- Twitter/X API(免费):每月500,000条推文,但每15分钟不超过15个请求
- Google搜索:每个IP每天约100个请求,无需授权
- Wildberries、Ozon:激进的速率限制——每分钟30-50个请求后封锁
- GitHub API:无令牌每小时60个请求,有令牌每小时5000个请求
- Cloudflare保护的网站:每分钟10-20个请求后可能会被封锁
如果您需要从市场收集100,000个商品卡片或实时监控价格——延迟根本无济于事。需要另一种架构。在这里,代理不仅是一个选项,而是一个必要条件。
重要的是要理解:速率限制与IP地址绑定。如果您有100个不同的IP——您实际上拥有100个独立的“配额”。这就是通过代理绕过限制的关键原则。
代理如何解决请求限制问题
机制很简单:每个请求都通过不同的IP地址发送到目标服务器。从服务器的角度来看——这是不同的用户。每个用户的配额几乎不会消耗,因此不会发生封锁。
让我们通过一个具体的例子来看看没有代理和使用代理池之间的区别。假设服务器允许每个IP每分钟10个请求:
| 场景 | 每分钟请求数 | 封锁 | 处理10,000个请求的时间 |
|---|---|---|---|
| 一个IP,无代理 | 10 | 是的,在10个请求后 | 约16小时 |
| 10个代理,轮换 | 100 | 没有 | 约1.7小时 |
| 100个代理,轮换 | 1000 | 没有 | 约10分钟 |
除了扩展带宽,代理在处理速率限制时还提供了几个额外的好处:
- 会话隔离 — 如果一个IP被封禁,其他IP仍然可以工作
- 地理分布 — 请求来自不同地区,降低了可疑性
- 粘性会话 — 在多步骤场景(授权 + 操作)中“粘附”到一个IP的能力
- 负载控制 — 可以精确控制每个IP的请求数量,而不超过限制
选择适合您任务的代理类型
并非所有代理在对抗速率限制方面都同样有效。选择类型取决于目标网站、请求量和预算。我们将讨论三种主要类型:
住宅代理
这些是来自真实家庭用户的IP地址。它们看起来像普通的互联网流量,并且很少被封锁。住宅代理是针对具有激进保护的网站的最佳选择:市场(Wildberries、Ozon)、社交网络、Cloudflare保护的资源。主要缺点是价格比数据中心代理高。
移动代理
来自移动运营商的IP地址(3G/4G/5G)。它们的特点是一个IP可以被成千上万的真实用户同时使用,因此网站极不愿意封锁这样的地址。移动代理在住宅代理开始被封锁的地方表现最佳——例如,在高频率解析Instagram或与分析连接类型的平台的API工作时。
数据中心代理
快速且便宜的来自服务器数据中心的IP。非常适合解析没有严重保护的网站:开放API、新闻聚合器、公共数据库。对于速率限制的任务,需要更多的代理(因为它们更容易被列入黑名单),但在正确的轮换下,它们可以很好地处理大量请求。更多信息请参见数据中心代理页面。
| 代理类型 | 匿名性 | 速度 | 价格 | 最佳场景 |
|---|---|---|---|---|
| 住宅代理 | 非常高 | 中等 | $$ | 市场、社交网络、Cloudflare |
| 移动代理 | 最高 | 中等 | $$$ | Instagram API、高频解析 |
| 数据中心 | 中等 | 高 | $ | 开放API、公共数据 |
IP轮换策略:每请求、粘性会话、轮询
仅仅拥有代理并不能解决问题——重要的是如何正确管理它们。存在几种轮换策略,每种策略适合不同的场景。
每请求轮换(每个请求使用新IP)
每个HTTP请求都通过新的IP地址发送。这是绕过速率限制的最激进策略——服务器根本来不及为一个IP累积计数器。适合于:
- 解析商品卡片(每个卡片是一个单独的请求)
- 从搜索引擎收集数据
- 任何不需要会话的无状态请求
粘性会话(会话期间使用固定IP)
在整个会话期间使用一个IP(通常为1-30分钟)。对于需要授权的场景至关重要:登录账户、执行操作、退出。如果在步骤之间更换IP——服务器可能会将会话标记为可疑并封锁。
轮询带IP请求限制
最精确的策略。您知道服务器的限制(例如,每分钟10个请求),并将请求分配到代理池中,以确保每个IP永远不会超过此阈值。需要实现一个队列,以考虑每个IP的最后请求时间。
所需代理数量的计算公式:
N代理 = (目标请求速度/分钟) ÷ (每个IP的服务器限制/分钟)
示例:需要500个请求/分钟,服务器限制——10/分钟 → 至少需要50个代理。
添加20%的备用以防封锁:总共60个代理。
Python代码示例:requests、aiohttp、Scrapy
让我们进入实践。以下是三个最流行的Python工具的现成模板。
1. requests + 手动代理轮换
最简单的选项——代理列表和每个请求随机选择:
import requests
import random
import time
PROXIES = [
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
# ... 添加所需数量
]
def get_random_proxy():
proxy = random.choice(PROXIES)
return {"http": proxy, "https": proxy}
def fetch_with_retry(url, max_retries=3):
for attempt in range(max_retries):
proxy = get_random_proxy()
try:
response = requests.get(
url,
proxies=proxy,
timeout=10,
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
)
if response.status_code == 429:
print(f"在{proxy}上速率限制,正在切换...")
time.sleep(1)
continue
return response
except requests.RequestException as e:
print(f"尝试{attempt+1}失败:{e}")
time.sleep(2)
return None
# 使用
urls = ["https://example.com/item/1", "https://example.com/item/2"]
for url in urls:
result = fetch_with_retry(url)
if result:
print(f"成功:{url} — {len(result.text)}字节")
2. 考虑速率限制的智能代理池
更高级的选项——ProxyPool类,跟踪每个IP的最后使用时间,并不超过设定的限制:
import requests
import time
from collections import defaultdict
from threading import Lock
class ProxyPool:
def __init__(self, proxies, rate_limit=10, window=60):
"""
proxies: 字符串列表,格式为'http://user:pass@host:port'
rate_limit: 每个IP在窗口内的最大请求数
window: 时间窗口(秒)
"""
self.proxies = proxies
self.rate_limit = rate_limit
self.window = window
self.usage = defaultdict(list) # proxy -> [timestamps]
self.lock = Lock()
def get_available_proxy(self):
now = time.time()
with self.lock:
for proxy in self.proxies:
# 清除过期的时间戳
self.usage[proxy] = [
t for t in self.usage[proxy]
if now - t < self.window
]
if len(self.usage[proxy]) < self.rate_limit:
self.usage[proxy].append(now)
return {"http": proxy, "https": proxy}
return None # 所有代理都已耗尽限制
def fetch(self, url, **kwargs):
proxy = self.get_available_proxy()
if proxy is None:
print("所有代理都被速率限制,正在等待...")
time.sleep(5)
return self.fetch(url, **kwargs)
try:
response = requests.get(url, proxies=proxy, timeout=10, **kwargs)
return response
except requests.RequestException as e:
print(f"请求失败:{e}")
return None
# 使用
pool = ProxyPool(
proxies=[
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
],
rate_limit=10, # 每个IP每分钟10个请求
window=60
)
for i in range(100):
r = pool.fetch(f"https://example.com/page/{i}")
if r:
print(f"页面 {i}: {r.status_code}")
3. 使用aiohttp进行异步解析
异步方法允许并行使用数十个代理而不阻塞线程:
import asyncio
import aiohttp
import itertools
PROXIES = [
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
]
proxy_cycle = itertools.cycle(PROXIES)
async def fetch(session, url, proxy):
try:
async with session.get(
url,
proxy=proxy,
timeout=aiohttp.ClientTimeout(total=10)
) as response:
if response.status == 429:
await asyncio.sleep(2)
return None
return await response.text()
except Exception as e:
print(f"错误:{e}")
return None
async def main(urls):
connector = aiohttp.TCPConnector(limit=50)
async with aiohttp.ClientSession(connector=connector) as session:
tasks = [
fetch(session, url, next(proxy_cycle))
for url in urls
]
results = await asyncio.gather(*tasks)
return results
urls = [f"https://example.com/item/{i}" for i in range(200)]
results = asyncio.run(main(urls))
print(f"收集到:{sum(1 for r in results if r is not None)} 页")
4. 使用Scrapy通过中间件进行轮换
对于Scrapy,有一个现成的解决方案——scrapy-rotating-proxies。但您也可以编写自己的中间件:
# middlewares.py
import random
class RotatingProxyMiddleware:
def __init__(self, proxies):
self.proxies = proxies
@classmethod
def from_crawler(cls, crawler):
return cls(proxies=crawler.settings.getlist("PROXY_LIST"))
def process_request(self, request, spider):
proxy = random.choice(self.proxies)
request.meta["proxy"] = proxy
def process_response(self, request, response, spider):
if response.status == 429:
spider.logger.warning(f"速率限制,代理:{request.meta.get('proxy')}")
# 可以添加逻辑来排除问题代理
return response
# settings.py
PROXY_LIST = [
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
]
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.RotatingProxyMiddleware": 350,
}
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 10
Node.js代码示例:axios、got、Puppeteer
Node.js是自动化浏览器和处理API的热门选择。以下是使用代理的现成模式。
1. axios与代理轮换
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const proxies = [
'http://user:[email protected]:8080',
'http://user:[email protected]:8080',
'http://user:[email protected]:8080',
];
let proxyIndex = 0;
function getNextProxy() {
const proxy = proxies[proxyIndex % proxies.length];
proxyIndex++;
return proxy;
}
async function fetchWithProxy(url, retries = 3) {
for (let i = 0; i < retries; i++) {
const proxyUrl = getNextProxy();
const agent = new HttpsProxyAgent(proxyUrl);
try {
const response = await axios.get(url, {
httpsAgent: agent,
httpAgent: agent,
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
},
});
return response.data;
} catch (error) {
if (error.response?.status === 429) {
console.log(`速率限制,正在切换代理...`);
await new Promise(r => setTimeout(r, 1000));
continue;
}
console.error(`尝试${i + 1}失败:`, error.message);
}
}
return null;
}
// 使用
(async () => {
const urls = Array.from({length: 50}, (_, i) => `https://example.com/item/${i}`);
const results = await Promise.allSettled(
urls.map(url => fetchWithProxy(url))
);
const successful = results.filter(r => r.status === 'fulfilled' && r.value).length;
console.log(`成功:${successful}/${urls.length}`);
})();
2. Puppeteer与代理和绕过速率限制
对于具有JavaScript渲染和Cloudflare保护的网站,需要无头浏览器:
const puppeteer = require('puppeteer');
const proxies = [
'proxy1.example.com:8080',
'proxy2.example.com:8080',
];
async function scrapeWithProxy(url, proxyHost) {
const browser = await puppeteer.launch({
args: [
`--proxy-server=${proxyHost}`,
'--no-sandbox',
'--disable-setuid-sandbox',
],
headless: true,
});
const page = await browser.newPage();
// 代理认证
await page.authenticate({
username: 'user',
password: 'pass',
});
// 设置真实的User-Agent
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
);
try {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
// 检查速率限制
const status = await page.evaluate(() => document.title);
if (status.includes('429') || status.includes('Too Many')) {
console.log('速率限制,需要切换代理');
return null;
}
const data = await page.evaluate(() => {
return document.querySelector('.price')?.textContent || null;
});
return data;
} finally {
await browser.close();
}
}
// 按任务轮换
(async () => {
const urls = ['https://example.com/product/1', 'https://example.com/product/2'];
for (let i = 0; i < urls.length; i++) {
const proxy = proxies[i % proxies.length];
const result = await scrapeWithProxy(urls[i], proxy);
console.log(`${urls[i]}: ${result}`);
await new Promise(r => setTimeout(r, 500)); // 小延迟
}
})();
高级技术:头部、指纹、绕过Cloudflare
更换IP是必要的,但并不总是足够的条件。现代保护系统分析请求的多个参数。让我们讨论一下还需要考虑哪些因素。
HTTP头部:最小必需集
没有正常头部的请求即使更换IP也会被视为机器人。始终添加:
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Cache-Control": "max-age=0",
}
处理Retry-After头部
在响应429时,服务器通常会指明需要等待的时间。正确处理此头部可以避免浪费请求:
def handle_rate_limit(response):
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
if retry_after:
wait_time = int(retry_after)
print(f"速率限制。等待{wait_time}秒...")
time.sleep(wait_time + 1) # +1秒缓冲
else:
# 如果没有头部,则进行指数延迟
time.sleep(min(2 ** attempt, 60))
return True
return False
TLS指纹及其绕过方法
高级系统(Cloudflare、Akamai、PerimeterX)分析TLS指纹——您TLS连接的唯一“指纹”。标准库requests具有易于识别的指纹。解决方案:
- curl_cffi(Python)——在TLS级别模拟Chrome/Firefox的指纹
- tls-client(Go/Python)——类似工具,支持不同的浏览器配置文件
- Playwright/Puppeteer——真实浏览器,默认情况下具有理想的指纹
# pip install curl-cffi
from curl_cffi import requests as cffi_requests
response = cffi_requests.get(
"https://cloudflare-protected-site.com/api/data",
impersonate="chrome120", # 模拟Chrome 120
proxies={"https": "http://user:[email protected]:8080"}
)
print(response.json())
管理cookies和会话
如果网站使用cookies跟踪会话,则在不更换cookies的情况下更换IP是没有意义的。在切换代理时,始终创建一个新的会话:
import requests
def create_fresh_session(proxy_url):
"""为每个代理创建一个具有干净cookies的新会话"""
session = requests.Session()
session.proxies = {"http": proxy_url, "https": proxy_url}
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
})
# cookies不会从上一个会话中转移
return session
# 对于每个新IP——新的会话
for proxy in proxies:
session = create_fresh_session(proxy)
response = session.get("https://example.com/protected-page")
# 处理响应...
使用代理和速率限制时的常见错误
即使使用正确配置的代理,开发者也经常会犯同样的错误。以下是最常见的错误及其避免方法。
检查清单:在启动解析器之前检查什么
- ☐ 添加了真实的HTTP头部(User-Agent、Accept、Accept-Language)
- ☐ 切换代理时创建新的会话(新的cookies)
- ☐ 处理429、503、403状态,具有重试逻辑
- ☐ 实现请求之间的延迟(至少100-500毫秒)
- ☐ 代理数量与目标请求速度相符
- ☐ 启动前检查代理的工作状态(健康检查)
- ☐ 记录每个代理的错误和统计信息
- ☐ 为请求设置超时(不超过15-30秒)
错误1:使用“死”代理
在将代理添加到池中之前,始终检查代理,并在工作期间定期检查。一个不工作的代理在循环中——就是丢失请求和超时:
def check_proxy(proxy_url, test_url="https://httpbin.org/ip", timeout=5):
try:
r = requests.get(
test_url,
proxies={"http": proxy_url, "https": proxy_url},
timeout=timeout
)
return r.status_code == 200
except:
return False
# 启动前过滤工作代理
working_proxies = [p for p in PROXIES if check_proxy(p)]
print(f"工作代理:{len(working_proxies)}/{len(PROXIES)}")
错误2:忽视协议类型
HTTP代理不能直接代理HTTPS流量(仅通过CONNECT)。SOCKS5代理在传输层工作,支持任何协议。对于大多数现代网站,请使用SOCKS5或HTTPS代理:
# SOCKS5代理在requests中(需要pip install requests[socks])
proxies = {
"http": "socks5://user:[email protected]:1080",
"https": "socks5://user:[email protected]:1080",
}
# HTTPS代理
proxies = {
"http": "https://user:[email protected]:8080",
"https": "https://user:[email protected]:8080",
}
错误3:缺乏指数退避
如果在429后立即重试请求——您只会加剧情况。正确的策略是指数延迟和抖动(随机偏差):
import random
def exponential_backoff(attempt, base=1, max_wait=60):
"""
attempt: 尝试次数(从0开始)
base: 基础延迟(秒)
max_wait: 最大延迟
"""
wait = min(base * (2 ** attempt), max_wait)
# 抖动±25%以防止雷霆集群
jitter = wait * 0.25 * random.uniform(-1, 1)
return wait + jitter
# 在重试逻辑中使用
for attempt in range(5):
response = requests.get(url, proxies=proxy)
if response.status_code == 429:
wait = exponential_backoff(attempt)
print(f"速率限制。等待{wait:.1f}秒(尝试{attempt+1})")
time.sleep(wait)
else:
break
错误4:所有代理使用一个线程
如果您有50个代理,但只有一个执行线程——您同时使用的最多是一个代理。使用ThreadPoolExecutor或异步方法并行使用整个池:
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch_url(args):
url, proxy = args
try:
r = requests.get(url, proxies={"https": proxy}, timeout=10)
return url, r.status_code, len(r.text)
except Exception as e:
return url, None, str(e)
# 并行使用所有代理
tasks = [(url, proxies[i % len(proxies)]) for i, url in enumerate(urls)]
with ThreadPoolExecutor(max_workers=len(proxies)) as executor:
futures = {executor.submit(fetch_url, task): task for task in tasks}
for future in as_completed(futures):
url, status, size = future.result()
print(f"{url}: {status} ({size})")
结论和建议
速率限制是一个可解决的问题,只要以系统的方式处理。以下是本指南的关键结论:
- 代理池,而不是单个代理——进行严肃工作的最小单位。代理数量由公式确定:目标速度 ÷ 每个IP的服务器限制。
- 轮换策略很重要——无状态请求使用每请求,授权场景使用粘性会话。
- IP不是唯一参数——头部、cookies、TLS指纹和行为模式也被保护系统分析。
- 正确处理429——指数退避、Retry-After头部、封锁时切换代理。
- 代理类型取决于目标——数据中心代理用于开放API,住宅代理用于市场,移动代理用于最大保护。
如果您正在处理市场解析(Wildberries、Ozon)、从受保护的API收集数据或以高速度进行自动化——我们建议您从住宅代理开始:它们提供了匿名性和速度之间的最佳平衡,其IP地址几乎不会被列入黑名单。对于需要在高请求频率下最大限度抵御封锁的任务,值得考虑移动代理——它们的IP由成千上万的真实用户共享,这使得任何网站都极不愿意封锁。