
scrapy 的 `parse` 方法必须显式 `yield` 所有后续请求,若将请求生成逻辑拆分为子函数但未逐层 `yield`,这些请求将被丢弃,导致爬虫停止递归抓取。
在 Scrapy 中,parse 方法不仅是数据解析入口,更是请求调度的唯一出口。其返回值(或 yield 产出)会被 Scrapy 引擎捕获并加入调度队列;任何未被 yield 的 scrapy.Request 对象都将被静默丢弃——这正是重构后代码失效的根本原因。
回顾原始有效代码:
def parse(self, response, **kwargs):
yield ScrapyItem(...) # ✅ 显式产出 Item
for link in self.extract_links(response):
yield scrapy.Request(...) # ✅ 显式产出 Request → 进入队列所有 Request 均由 parse 直接 yield,Scrapy 可完整感知并调度。
而重构后的错误版本中:
def parse(self, response, **kwargs):
yield ScrapyItem(...)
self.extract_and_follow_links(response) # ❌ 仅调用,未 yield 返回值
def extract_and_follow_links(self, response):
links = self.extract_links(response)
return self.follow_links(response, links) # ✅ 返回 generator,但未被消费
def follow_links(self, response, links):
for link in links:
yield scrapy.Request(...) # ✅ generator 内部 yield,但外部未迭代follow_links() 是一个生成器函数(generator function),它返回的是一个惰性迭代器对象,而非立即执行的请求列表。若不主动遍历该迭代器(如用 for req in gen: yield req)或直接 yield from gen,其中的 yield scrapy.Request(...) 永远不会触发,请求也就永远不会提交给 Scrapy 调度器。
✅ 正确修复方式有两种(推荐后者,更简洁清晰):
方式一:显式循环 + yield
def parse(self, response, **kwargs):
self.logger.info(f"Parse: Processing {response.url}")
yield ScrapyItem(
source=response.meta["source"],
url=response.url,
html=response.text,
)
# 关键:迭代并 yield 子函数返回的所有请求
for request in self.extract_and_follow_links(response):
yield request
def extract_and_follow_links(self, response):
links = self.extract_links(response)
self.logger.info(f"Extracted {len(links)} links from {response.url}")
# TODO: Save links to database
return self.follow_links(response, links) # 返回 generator
def follow_links(self, response, links):
self.logger.info(f"Following {len(links)} links from {response.url}")
for link in links:
self.logger.info(f"Following link: {link.url}")
yield scrapy.Request(
url=link.url,
callback=self.parse,
meta={"source": response.meta["source"]},
)方式二(推荐):使用 yield from(Python 3.3+)
def parse(self, response, **kwargs):
self.logger.info(f"Parse: Processing {response.url}")
yield ScrapyItem(
source=response.meta["source"],
url=response.url,
html=response.text,
)
# 一行替代循环,语义更明确
yield from self.extract_and_follow_links(response)
def extract_and_follow_links(self, response):
links = self.extract_links(response)
self.logger.info(f"Extracted {len(links)} links from {response.url}")
# TODO: Save links to database
yield from self.follow_links(response, links) # 直接委托生成
def follow_links(self, response, links):
self.logger.info(f"Following {len(links)} links from {response.url}")
for link in links:
self.logger.info(f"Following link: {link.url}")
yield scrapy.Request(
url=link.url,
callback=self.parse,
meta={"source": response.meta["source"]},
)⚠️ 注意事项:
- Scrapy 不会自动“展开”嵌套生成器;yield 和 yield from 是显式传递控制权的必要语法。
- 若在子函数中需同时处理 Item 和 Request(如先存链接再发请求),仍须确保所有 Request 最终由 parse 或其直接调用链 yield 出来。
- 日志中看到 DropItem 并非因 parse 报错,而是因为 start_urls 页面成功产出 Item 后,无后续请求入队,Scrapy 认为任务结束,自然终止爬取。
总结:Scrapy 的请求流是严格基于 yield 链的显式数据流。重构时务必保持“生成器链”的完整性——每个中间函数若返回 generator,上层必须用 yield from 或显式迭代 yield 其产出,否则请求将永远停留在内存中,无法进入 Scrapy 的异步调度核心。










