如何结合Scrapy与Selenium爬取网页?为何page_source缺失目标内容?
问题分析:为什么page_source找不到内容但find_element可以?
这种情况大概率是动态内容的渲染时机不匹配导致的:
- 当你调用
driver.get(url)后,浏览器会先加载页面的基础HTML结构(也就是page_source当时返回的内容),但目标内容可能是通过AJAX请求异步获取后,再由JavaScript插入到DOM中的。这时候你立刻获取page_source,异步内容还没加载完成,自然找不到。 - 而
find_element_by_css_selector()方法(注意这个方法在Selenium 4+已经被弃用,推荐用find_element(By.CSS_SELECTOR, ...))会尝试查找元素——如果你的代码执行到这一步时,刚好异步内容已经加载完成,或者Selenium隐式等待(默认是0,但如果之前设置过driver.implicitly_wait(10))生效了,就能成功定位到元素。
另外还有一种小概率情况:目标内容存在于<iframe>标签中,此时page_source是主页面的HTML,而你调用find_element前可能无意中切换到了iframe(不过从你的代码看这种可能性很低)。
如何结合Scrapy与Selenium爬取目标贴吧内容?
要实现Scrapy和Selenium的结合,最优雅的方式是通过自定义下载中间件,让Scrapy在需要时调用Selenium渲染页面,而不是直接在Spider里写Selenium代码。下面针对你提供的贴吧链接给出完整实现步骤:
1. 准备工作
确保你已经安装了所需依赖:
pip install scrapy selenium
同时下载对应版本的ChromeDriver,并确保它在系统PATH中,或者在代码中指定路径。
2. 编写自定义下载中间件
在你的Scrapy项目的middlewares.py中添加以下代码:
from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from scrapy.http import HtmlResponse import time class SeleniumMiddleware: def __init__(self): # 配置Chrome无头模式,减少资源占用 chrome_options = Options() chrome_options.add_argument("--headless=new") chrome_options.add_argument("--disable-gpu") chrome_options.add_argument("--no-sandbox") chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36") self.driver = webdriver.Chrome(options=chrome_options) # 设置显式等待超时时间 self.wait = WebDriverWait(self.driver, 15) def process_request(self, request, spider): # 只对指定的贴吧请求使用Selenium渲染 if "tieba.baidu.com/p/" in request.url: self.driver.get(request.url) # 等待目标元素加载完成(这里对应贴吧楼层的回复内容选择器) try: self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".d_post_content.j_d_post_content"))) # 等待页面完全渲染(可选,针对需要滚动加载的内容) time.sleep(2) # 获取当前渲染后的页面源码 body = self.driver.page_source return HtmlResponse( url=self.driver.current_url, body=body, encoding='utf-8', request=request ) except Exception as e: spider.logger.error(f"Selenium渲染失败: {str(e)}") return None # 其他请求使用默认下载器 return None def __del__(self): # 关闭浏览器,释放资源 self.driver.quit()
3. 启用中间件
在项目的settings.py中,修改DOWNLOADER_MIDDLEWARES配置:
DOWNLOADER_MIDDLEWARES = { # 替换成你的项目名 'your_project_name.middlewares.SeleniumMiddleware': 543, # 禁用默认的HttpCompressionMiddleware(可选,避免编码问题) 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': None, }
4. 编写Spider
在spiders目录下创建tieba_spider.py:
import scrapy class TiebaSpider(scrapy.Spider): name = 'tieba' allowed_domains = ['tieba.baidu.com'] start_urls = ['http://tieba.baidu.com/p/5513911529'] def parse(self, response): # 从渲染后的页面中提取目标内容(红圈中的楼层回复文本) posts = response.css(".l_post.j_l_post.l_post_bright") for post in posts: yield { '楼层作者': post.css(".p_author_name::text").get().strip(), '回复内容': post.css(".d_post_content.j_d_post_content::text").get().strip(), '发布时间': post.css(".tail-info::text").getall()[1] if len(post.css(".tail-info::text").getall())>=2 else None }
5. 运行爬虫
在终端执行:
scrapy crawl tieba -o tieba_posts.json
关键注意事项
- 等待策略:一定要使用显式等待(
WebDriverWait)代替固定time.sleep(),这样既保证元素加载完成,又不会浪费不必要的时间。 - Selenium版本兼容:如果使用Selenium 4+,请确保导入
from selenium.webdriver.common.by import By,并使用find_element(By.CSS_SELECTOR, selector)代替旧的find_element_by_css_selector()方法。 - 反爬处理:贴吧有反爬机制,建议添加合理的User-Agent,必要时可以配置登录后的Cookie来绕过限制。
- 资源释放:中间件中通过
__del__方法关闭浏览器,避免长时间运行导致内存泄漏。
内容的提问来源于stack exchange,提问作者YoarkYANG
相关产品推荐
相关产品推荐

