Scrapy下载中间件中如何返回多次滚动后的HtmlResponse?
解决思路
Scrapy的DownloaderMiddleware的process_request方法只能返回单个Response对象或者None,没法像Spider的parse方法那样yield多个结果,所以你之前的两种尝试都走不通。给你两个可行的解决方案:
方案一:把滚动逻辑移到Spider中(推荐)
直接在Spider的parse方法里用Selenium控制滚动,每次滚动后直接处理当前页面的内容,这样可以灵活处理每次滚动后的page_source,还能直接yield解析出的数据或者后续请求。
示例代码:
import scrapy from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time import logging class YourSpider(scrapy.Spider): name = 'your_spider' start_urls = ['目标网址'] def __init__(self): self.driver = webdriver.Chrome() # 根据你的浏览器配置调整 self.wait = WebDriverWait(self.driver, 10) def closed(self, reason): self.driver.quit() # 爬虫结束时关闭浏览器,避免资源泄漏 def parse(self, response): self.driver.get(response.url) for i in range(20): try: # 执行滚动操作 self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") logging.error(f'第{i+1}次滚动') # 等待页面内容更新(等待条件要根据页面实际情况调整) self.wait.until(EC.presence_of_element_located((By.XPATH, '//div/a[@role = "link"]'))) time.sleep(2) logging.error(f'等待第{i+1}次滚动完成') # 获取当前页面源码并解析处理 current_page_source = self.driver.page_source selector = scrapy.Selector(text=current_page_source) # 示例:提取数据并yield target_items = selector.xpath('//你的目标元素XPath').getall() for item in target_items: yield {'content': item} except Exception as e: logging.error(f'第{i+1}次滚动失败: {str(e)}') break
方案二:在Middleware中收集所有滚动后的page_source,一次性传给Spider
如果一定要用DownloaderMiddleware,那可以在滚动过程中把每次的page_source收集到列表里,然后放到返回的HtmlResponse的meta字段中,Spider拿到后再遍历这个列表处理每个页面的内容。
修改后的Middleware代码:
from scrapy.http import HtmlResponse from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time import logging class SeleniumScrollMiddleware: def __init__(self): self.driver = webdriver.Chrome() self.wait = WebDriverWait(self.driver, 10) def process_request(self, request, spider): # 只处理标记了需要滚动的请求 if 'scroll_needed' in request.meta: self.driver.get(request.url) page_sources = [] # 收集每次滚动后的页面源码 for i in range(20): try: self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") logging.error(f'scroll{i+1}times') self.wait.until(EC.presence_of_element_located((By.XPATH, '//div/a[@role = "link"]'))) time.sleep(2) logging.error(f'waiting scroll{i+1}times') # 保存当前页面源码到列表 current_source = self.driver.page_source page_sources.append(current_source) logging.error(f'get page_source{i+1}times') except Exception as e: logging.error(f'scroll failed: {str(e)}') break # 返回包含所有page_source的Response return HtmlResponse( body=self.driver.page_source, # 这里的body仅作为占位,核心是meta里的列表 url=request.url, encoding='utf-8', meta={'page_sources': page_sources} ) return None
然后在Spider里这样处理:
def parse(self, response): page_sources = response.meta.get('page_sources', []) for source in page_sources: selector = scrapy.Selector(text=source) # 解析每个页面的内容并yield target_items = selector.xpath('//你的目标元素XPath').getall() for item in target_items: yield {'content': item}
注意事项
- 等待条件要根据页面实际更新的元素调整,不要盲目套用固定XPath,确保等待的是滚动后确实会更新的内容。
time.sleep可以根据页面加载速度调整,或者用更智能的等待方式(比如等待元素文本变化)代替固定休眠。- 记得在爬虫结束时关闭Selenium的driver,避免资源泄漏。
内容的提问来源于stack exchange,提问作者Jiong Zeng
相关产品推荐
相关产品推荐

