Skip to content

Commit 356d363

Browse files
author
ShellMonster
committed
feat: Add per-domain QPS throttling control
## 功能概述 为 feapder 框架添加域名级 QPS(每秒请求数)限制功能,支持为不同域名设置独立的请求速率限制。 ## 核心特性 - ✅ 支持所有 Spider 类型(AirSpider、Spider、TaskSpider、BatchSpider) - ✅ 灵活的域名匹配策略(精确匹配、通配符匹配、www回退) - ✅ 分布式友好(基于 Redis 的多机器 QPS 配额共享) - ✅ 非阻塞设计(延迟调度机制,不阻塞工作线程) - ✅ 开箱即用(只需配置,无需编写代码) - ✅ 容错能力强(Redis 异常时自动降级) ## 技术实现 ### 令牌桶算法 - **AirSpider**: 本地内存版令牌桶(线程安全) - **分布式 Spider**: Redis 分布式令牌桶(Lua 脚本保证原子性) ### 核心组件 1. `LocalTokenBucket`: 本地内存令牌桶 2. `RedisTokenBucket`: Redis 分布式令牌桶 3. `DomainRateLimiter`: 统一管理器,自动选择令牌桶类型 ## 使用示例 ```python class MySpider(feapder.Spider): __custom_setting__ = dict( DOMAIN_RATE_LIMIT_ENABLE=True, DOMAIN_RATE_LIMIT_DEFAULT=10, DOMAIN_RATE_LIMIT_RULES={ "baidu.com": 5, "*.google.com": 8, } ) ``` ## 修改文件 ### 新增文件 - `feapder/utils/rate_limiter.py`: 令牌桶算法实现(~300行) - `docs/usage/域名级QPS限制.md`: 完整使用文档 - `test_qps_limit.py`: 功能测试脚本 ### 修改文件 - `feapder/setting.py`: 新增 QPS 相关配置项 - `feapder/network/request.py`: 在 get_response() 中添加 QPS 检查逻辑 - `feapder/core/scheduler.py`: 传递 redis_key 给 Request 类 - `feapder/core/parser_control.py`: 注入 request_buffer 到请求对象 - `feapder/templates/*.tmpl`: 在 4 个模板中添加 QPS 配置示例 - `docs/_sidebar.md`: 添加文档导航 ## 测试 ✅ 域名提取功能测试通过 ✅ 本地令牌桶算法测试通过 ✅ 代码语法检查通过 ## 相关文档 - 技术方案: `域名级QPS限制技术方案.md` - 使用文档: `docs/usage/域名级QPS限制.md`
1 parent 100cde4 commit 356d363

12 files changed

Lines changed: 953 additions & 1 deletion

docs/_sidebar.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* [分布式爬虫-Spider](usage/Spider.md)
1414
* [任务爬虫-TaskSpider](usage/TaskSpider.md)
1515
* [批次爬虫-BatchSpider](usage/BatchSpider.md)
16+
* [域名级QPS限制](usage/域名级QPS限制.md)
1617
* [爬虫集成](usage/爬虫集成.md)
1718

1819
* 使用进阶

docs/usage/域名级QPS限制.md

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
# 域名级QPS限制
2+
3+
域名级QPS限制功能允许你为不同的域名设置独立的请求速率限制(Queries Per Second),防止爬虫对目标网站造成过大压力,同时避免被反爬虫机制封禁。
4+
5+
## 1. 功能特性
6+
7+
-**支持所有Spider类型**:AirSpider、Spider、TaskSpider、BatchSpider
8+
-**灵活的域名匹配**:支持精确域名、通配符域名(`*.google.com`)、www回退策略
9+
-**分布式友好**:多台机器共享QPS配额(基于Redis)
10+
-**非阻塞设计**:延迟调度机制,不会卡住工作线程
11+
-**开箱即用**:只需配置,无需编写代码
12+
-**容错能力强**:Redis异常时自动降级,不影响爬虫运行
13+
14+
## 2. 工作原理
15+
16+
QPS限制基于**令牌桶算法**实现:
17+
18+
- **AirSpider**:使用本地内存版令牌桶(线程安全)
19+
- **Spider/TaskSpider/BatchSpider**:使用Redis分布式令牌桶(支持多机器共享配额)
20+
21+
当请求超过配置的QPS限制时,会自动延迟执行,而不是阻塞线程。
22+
23+
## 3. 基础使用
24+
25+
### 3.1 AirSpider示例
26+
27+
```python
28+
import feapder
29+
30+
class MySpider(feapder.AirSpider):
31+
__custom_setting__ = dict(
32+
# 启用域名级QPS限制
33+
DOMAIN_RATE_LIMIT_ENABLE=True,
34+
# 默认每个域名10 QPS
35+
DOMAIN_RATE_LIMIT_DEFAULT=10,
36+
# 特定域名的QPS规则
37+
DOMAIN_RATE_LIMIT_RULES={
38+
"baidu.com": 5, # 百度主域名限制5 QPS
39+
"api.baidu.com": 20, # 百度API限制20 QPS
40+
"*.google.com": 8, # 所有谷歌系域名8 QPS
41+
}
42+
)
43+
44+
def start_requests(self):
45+
yield feapder.Request("https://www.baidu.com")
46+
yield feapder.Request("https://api.baidu.com/v1/data")
47+
yield feapder.Request("https://maps.google.com")
48+
49+
def parse(self, request, response):
50+
print(f"成功抓取: {request.url}")
51+
52+
if __name__ == "__main__":
53+
MySpider().start()
54+
```
55+
56+
### 3.2 Spider示例
57+
58+
```python
59+
import feapder
60+
61+
class MySpider(feapder.Spider):
62+
__custom_setting__ = dict(
63+
REDISDB_IP_PORTS="localhost:6379",
64+
REDISDB_USER_PASS="",
65+
REDISDB_DB=0,
66+
# QPS配置
67+
DOMAIN_RATE_LIMIT_ENABLE=True,
68+
DOMAIN_RATE_LIMIT_DEFAULT=10,
69+
DOMAIN_RATE_LIMIT_RULES={
70+
"baidu.com": 5,
71+
"zhihu.com": 3,
72+
}
73+
)
74+
75+
def start_requests(self):
76+
for i in range(100):
77+
yield feapder.Request(f"https://www.baidu.com/s?wd={i}")
78+
79+
def parse(self, request, response):
80+
print(f"成功抓取: {request.url}")
81+
82+
if __name__ == "__main__":
83+
MySpider(redis_key="test:qps").start()
84+
```
85+
86+
## 4. 配置详解
87+
88+
### 4.1 配置项说明
89+
90+
| 配置项 | 类型 | 默认值 | 说明 |
91+
|--------|------|--------|------|
92+
| `DOMAIN_RATE_LIMIT_ENABLE` | bool | False | 是否启用域名级QPS限制 |
93+
| `DOMAIN_RATE_LIMIT_DEFAULT` | int | 10 | 默认每个域名的QPS限制 |
94+
| `DOMAIN_RATE_LIMIT_RULES` | dict | {} | 特定域名的QPS规则 |
95+
96+
### 4.2 域名匹配规则
97+
98+
QPS配置按以下优先级匹配(从高到低):
99+
100+
1. **单个请求的 qps_limit 参数**(最高优先级)
101+
2. **精确域名匹配**(完全一致,包括www前缀)
102+
3. **通配符匹配**(支持 `*.domain` 格式)
103+
4. **www回退策略**(如果访问www.baidu.com未匹配,自动尝试baidu.com)
104+
5. **默认值** `DOMAIN_RATE_LIMIT_DEFAULT`
105+
106+
#### 示例1:简化配置(推荐)
107+
108+
只配置主域名,www会自动回退:
109+
110+
```python
111+
DOMAIN_RATE_LIMIT_RULES = {
112+
"baidu.com": 5, # www.baidu.com 和 baidu.com 都限制为 5 QPS
113+
"api.baidu.com": 10, # api.baidu.com 限制为 10 QPS
114+
}
115+
```
116+
117+
匹配结果:
118+
- `https://www.baidu.com` → 5 QPS(回退到 baidu.com)
119+
- `https://baidu.com` → 5 QPS(精确匹配)
120+
- `https://api.baidu.com` → 10 QPS(精确匹配)
121+
- `https://tieba.baidu.com` → 10 QPS(默认值)
122+
123+
#### 示例2:精确控制
124+
125+
区分www和非www:
126+
127+
```python
128+
DOMAIN_RATE_LIMIT_RULES = {
129+
"www.example.com": 20, # www流量大,限制宽松
130+
"example.com": 5, # 非www流量小,限制严格
131+
}
132+
```
133+
134+
匹配结果:
135+
- `https://www.example.com` → 20 QPS(精确匹配)
136+
- `https://example.com` → 5 QPS(精确匹配)
137+
138+
#### 示例3:通配符匹配
139+
140+
限制整个域名族群:
141+
142+
```python
143+
DOMAIN_RATE_LIMIT_RULES = {
144+
"*.google.com": 8, # 所有谷歌三级域名
145+
"*.amazonaws.com": 15, # 所有AWS服务
146+
}
147+
```
148+
149+
匹配结果:
150+
- `https://maps.google.com` → 8 QPS(通配符匹配)
151+
- `https://apis.google.com` → 8 QPS(通配符匹配)
152+
- `https://google.com` → 10 QPS(通配符不匹配无子域名的情况,使用默认值)
153+
154+
## 5. 高级用法
155+
156+
### 5.1 单个请求自定义QPS
157+
158+
可以为单个请求设置独立的QPS限制:
159+
160+
```python
161+
def start_requests(self):
162+
# 重要接口,限制1 QPS
163+
yield feapder.Request(
164+
"https://api.important.com/data",
165+
qps_limit=1 # 单独设置这个请求的QPS
166+
)
167+
168+
# 普通接口,使用默认配置
169+
yield feapder.Request("https://www.baidu.com")
170+
```
171+
172+
### 5.2 精细化域名控制
173+
174+
为不同级别的域名设置不同QPS:
175+
176+
```python
177+
DOMAIN_RATE_LIMIT_RULES = {
178+
# 主域名
179+
"example.com": 5,
180+
181+
# API子域名(通常可以承受更高QPS)
182+
"api.example.com": 20,
183+
184+
# CDN子域名(静态资源,可以更高)
185+
"cdn.example.com": 50,
186+
187+
# 通配符兜底
188+
"*.example.com": 3, # 其他子域名保守限制
189+
}
190+
```
191+
192+
### 5.3 多爬虫任务独立限速
193+
194+
不同的爬虫任务使用不同的 `redis_key`,QPS配额相互独立:
195+
196+
```python
197+
# 爬虫任务1:百度搜索,限制5 QPS
198+
class BaiduSpider(feapder.Spider):
199+
__custom_setting__ = dict(
200+
DOMAIN_RATE_LIMIT_ENABLE=True,
201+
DOMAIN_RATE_LIMIT_RULES={"baidu.com": 5}
202+
)
203+
204+
# 爬虫任务2:同时抓取百度,限制10 QPS(不冲突)
205+
class BaiduSpider2(feapder.Spider):
206+
__custom_setting__ = dict(
207+
DOMAIN_RATE_LIMIT_ENABLE=True,
208+
DOMAIN_RATE_LIMIT_RULES={"baidu.com": 10}
209+
)
210+
211+
if __name__ == "__main__":
212+
# 两个爬虫可以同时运行,各自有独立的QPS配额
213+
spider1 = BaiduSpider(redis_key="task1")
214+
spider2 = BaiduSpider2(redis_key="task2")
215+
216+
spider1.start()
217+
# spider2.start() # 可以在另一台机器上运行
218+
```
219+
220+
## 6. 注意事项
221+
222+
### 6.1 Redis配置要求
223+
224+
- **Spider/TaskSpider/BatchSpider** 需要配置Redis才能使用分布式QPS限制
225+
- **AirSpider** 使用本地内存,无需Redis
226+
227+
### 6.2 QPS计算方式
228+
229+
QPS = Queries Per Second(每秒请求数)
230+
231+
例如:配置 `"baidu.com": 5` 表示每秒最多发送5个请求到baidu.com
232+
233+
### 6.3 性能影响
234+
235+
- 本地令牌桶:几乎无性能损耗
236+
- Redis令牌桶:每次请求增加 1-10ms 延迟(取决于Redis网络延迟)
237+
- 对比HTTP请求耗时(通常100-1000ms),性能开销可忽略
238+
239+
### 6.4 容错机制
240+
241+
- Redis连接失败时,自动降级为放行所有请求
242+
- 不会因为QPS限制模块异常而导致爬虫停止
243+
244+
## 7. 调试与监控
245+
246+
### 7.1 查看QPS限制日志
247+
248+
启用DEBUG日志级别可以看到QPS限制的详细信息:
249+
250+
```python
251+
LOG_LEVEL = "DEBUG"
252+
```
253+
254+
日志输出示例:
255+
256+
```
257+
[QPS限制] 域名 baidu.com 达到限制 5 QPS, 延迟 0.20秒后重试
258+
```
259+
260+
### 7.2 验证QPS是否生效
261+
262+
可以通过记录请求时间来验证:
263+
264+
```python
265+
import time
266+
267+
class TestSpider(feapder.AirSpider):
268+
__custom_setting__ = dict(
269+
DOMAIN_RATE_LIMIT_ENABLE=True,
270+
DOMAIN_RATE_LIMIT_RULES={"httpbin.org": 2} # 2 QPS
271+
)
272+
273+
def parse(self, request, response):
274+
print(f"[{time.strftime('%H:%M:%S')}] 请求完成: {request.url}")
275+
```
276+
277+
如果配置正确,你会看到请求按照设定的QPS速率执行。
278+
279+
## 8. 常见问题
280+
281+
### Q1: 为什么配置了QPS限制,但请求还是很快?
282+
283+
**A:** 检查以下几点:
284+
1. 确认 `DOMAIN_RATE_LIMIT_ENABLE` 设置为 `True`
285+
2. 检查域名是否匹配(注意www前缀)
286+
3. 检查并发线程数 `SPIDER_THREAD_COUNT` 是否过大
287+
288+
### Q2: 多个域名同时爬取时,QPS如何计算?
289+
290+
**A:** 每个域名的QPS是独立计算的。例如:
291+
- 同时爬取 baidu.com(5 QPS)和 google.com(8 QPS)
292+
- 总QPS = 5 + 8 = 13 QPS
293+
294+
### Q3: AirSpider和Spider的QPS限制有什么区别?
295+
296+
**A:**
297+
- **AirSpider**:本地内存版,单机独立QPS配额
298+
- **Spider**:Redis分布式版,多台机器共享QPS配额
299+
300+
### Q4: 如何临时关闭QPS限制?
301+
302+
**A:** 设置 `DOMAIN_RATE_LIMIT_ENABLE=False` 即可
303+
304+
## 9. 最佳实践
305+
306+
### 9.1 推荐的QPS配置
307+
308+
根据目标网站类型,推荐以下QPS配置:
309+
310+
| 网站类型 | 推荐QPS | 说明 |
311+
|---------|---------|------|
312+
| 大型门户网站 | 5-10 | 如百度、新浪 |
313+
| API接口 | 10-50 | 取决于服务商限制 |
314+
| 小型网站 | 1-5 | 避免压力过大 |
315+
| CDN静态资源 | 20-100 | 通常限制较宽松 |
316+
317+
### 9.2 配置建议
318+
319+
1. **优先使用简化配置**:只配置主域名(如 `baidu.com`),让www自动回退
320+
2. **API独立配置**:API子域名通常需要更精确的QPS控制
321+
3. **通配符兜底**:使用通配符为未知子域名设置保守的默认QPS
322+
4. **逐步调整**:从保守的QPS开始,逐步提高直到找到最优值
323+
324+
### 9.3 监控与调优
325+
326+
```python
327+
import time
328+
329+
class MonitorSpider(feapder.Spider):
330+
request_times = []
331+
332+
def parse(self, request, response):
333+
# 记录请求时间
334+
self.request_times.append(time.time())
335+
336+
# 每100个请求统计一次QPS
337+
if len(self.request_times) >= 100:
338+
duration = self.request_times[-1] - self.request_times[0]
339+
actual_qps = len(self.request_times) / duration
340+
print(f"实际QPS: {actual_qps:.2f}")
341+
self.request_times = []
342+
```
343+
344+
## 10. 相关文档
345+
346+
- [Spider进阶](source_code/Spider进阶.md)
347+
- [配置文件](source_code/配置文件.md)
348+
- [命令行工具](command/cmdline.md)

feapder/core/parser_control.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ def deal_request(self, request):
7878
response = None
7979
request_redis = request["request_redis"]
8080
request = request["request_obj"]
81+
# 注入request_buffer,用于QPS限制时将请求放回队列
82+
request._request_buffer = self._request_buffer
8183

8284
del_request_redis_after_item_to_db = False
8385
del_request_redis_after_request_to_db = False

feapder/core/scheduler.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ def __init__(
7777
setattr(setting, key, value)
7878

7979
self._redis_key = redis_key or setting.REDIS_KEY
80+
# 将redis_key传递给Request类(用于QPS限制)
81+
from feapder.network.request import Request
82+
83+
Request.cached_redis_key = self._redis_key
84+
8085
if not self._redis_key:
8186
raise Exception(
8287
"""

0 commit comments

Comments
 (0)