不能直接用 list 做待爬队列,因其 pop(0) 为 o(n) 且非线程安全;应改用 queue.queue、redis 或 scrapy-redis;需分域名建队列、加权调度、标准化 url 后哈希去重,并通过 domainawarescheduler 实现多域名 bfs。

为什么不能直接用 list 做待爬队列
用 list 模拟队列(比如 queue.pop(0))在多域名场景下会迅速拖慢性能:每次 pop(0) 是 O(n) 时间复杂度,URL 数量过千后延迟明显。更关键的是,list 无法天然支持跨线程/进程安全的出队操作,多个爬虫线程同时 pop 可能漏掉 URL 或重复抓取。
真实项目中必须换用线程安全、O(1) 出队的数据结构:
-
queue.Queue(单机多线程,推荐) -
redis.Redis.lpop+rpush(分布式、需部署 Redis) -
scrapy-redis的SpiderQueue(Scrapy 生态首选)
如何按域名隔离并调度广度层级
多域名意味着不同站点的响应头、反爬策略、页面结构、链接规则都不同。如果混在同一个队列里广度展开,很容易出现:A 域名刚被封,B 域名的请求还在排队;或 A 域名的深度为 2,B 域名才刚进第 1 层,但队列已把 B 的子链接提前塞进去了。
正确做法是分域建队列,并加权调度:
- 每个域名维护独立的
queue.Queue实例,命名如queue_dytt8_net、queue_baidu_com - 主调度器轮询各域名队列,但按「剩余 URL 数量 × 域名权重」动态分配抓取配额(例如 dytt8.net 权重设为 0.7,baidu.com 设为 0.3)
- 每层广度抓取完成后,统一收集所有域名的新链接,再按域名归类、去重、入各自队列 —— 不是“全放一起再分”,而是“分好再进队”
去重必须跨域名做,但不能只靠 set
单纯用 set(url) 去重在多域名场景下会失效:同一路径 /movie/123 在 dytt8.net 和 imdb.com 是完全不同的资源,但字符串一样;反过来,https://dytt8.net/index.html 和 https://www.dytt8.net/index.html 是同一页面,却因 host 差异被当成两个 URL。
可靠方案是标准化 URL 后哈希:
- 用
urllib.parse.urlparse拆解,强制小写netloc,移除末尾/和 query 中的 session 参数(如sid=xxx) - 拼接
netloc + path + (normalized query)再做hashlib.sha256().hexdigest() - 把哈希值存进全局
Redis.set或本地sqlite,而不是内存set—— 否则重启就丢记录
Scrapy 中开启多域名 BFS 的最小配置
Scrapy 默认就是广度优先,但默认不区分域名。要让它真正“智能”支持多域名,得改两处:
- 在
start_requests()中显式为每个种子 URL 设置meta={'domain': 'dytt8.net'} - 在
parse()回调里,对提取到的每个href调用response.follow()时,带上meta={'domain': extracted_domain} - 在
settings.py加上:BFE_PRIORITY = 0(保持 BFS)DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'(确保去重逻辑生效)SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.FifoMemoryQueue'(FIFO 即 BFS)
最关键的其实是中间件:写一个 DomainAwareScheduler,在 enqueue_request 阶段根据 request.meta['domain'] 把请求路由到对应内存队列,否则 Scrapy 还是会把所有域名混排。
实际跑起来你会发现,最难的不是代码,而是域名行为差异 —— 某些站禁止爬首页以外的路径,某些站 robots.txt 动态返回不同规则,这些必须在入队前做预检,而不是等请求发出去再 403。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











