如何在scrapy中集成selenium爬取网页的方法


Posted in Python onNovember 18, 2020

1.背景

  • 我们在爬取网页时一般会使用到三个爬虫库:requests,scrapy,selenium。requests一般用于小型爬虫,scrapy用于构建大的爬虫项目,而selenium主要用来应付负责的页面(复杂js渲染的页面,请求非常难构造,或者构造方式经常变化)。
  • 在我们面对大型爬虫项目时,肯定会优选scrapy框架来开发,但是在解析复杂JS渲染的页面时,又很麻烦。 尽管使用selenium浏览器渲染来抓取这样的页面很方便,这种方式下,我们不需要关心页面后台发生了怎样的请求,也不需要分析整个页面的渲染过程,我们只需要关心页面最终结果即可,可见即可爬,但是selenium的效率又太低。
  • 所以,如果可以在scrapy中,集成selenium,让selenium负责复杂页面的爬取,那么这样的爬虫就无敌了,可以爬取任何网站了。

 2. 环境

  • python 3.6.1
  • 系统:win7
  • IDE:pycharm
  • 安装过chrome浏览器
  • 配置好chromedriver(设置好环境变量)
  • selenium 3.7.0
  • scrapy 1.4.0

3.原理分析

3.1. 分析request请求的流程

首先看一下scrapy最新的架构图:

如何在scrapy中集成selenium爬取网页的方法

部分流程:

第一:爬虫引擎生成requests请求,送往scheduler调度模块,进入等待队列,等待调度。

第二:scheduler模块开始调度这些requests,出队,发往爬虫引擎。

第三:爬虫引擎将这些requests送到下载中间件(多个,例如加header,代理,自定义等等)进行处理。

第四:处理完之后,送往Downloader模块进行下载。从这个处理过程来看,突破口就在下载中间件部分,用selenium直接处理掉request请求。

3.2. requests和response中间处理件源码分析

相关代码位置:

如何在scrapy中集成selenium爬取网页的方法

源码解析:

# 文件:E:\Miniconda\Lib\site-packages\scrapy\core\downloader\middleware.py
"""
Downloader Middleware manager

See documentation in docs/topics/downloader-middleware.rst
"""
import six

from twisted.internet import defer

from scrapy.http import Request, Response
from scrapy.middleware import MiddlewareManager
from scrapy.utils.defer import mustbe_deferred
from scrapy.utils.conf import build_component_list


class DownloaderMiddlewareManager(MiddlewareManager):

  component_name = 'downloader middleware'

  @classmethod
  def _get_mwlist_from_settings(cls, settings):
    # 从settings.py或这custom_setting中拿到自定义的Middleware中间件
    '''
    'DOWNLOADER_MIDDLEWARES': {
      'mySpider.middlewares.ProxiesMiddleware': 400,
      # SeleniumMiddleware
      'mySpider.middlewares.SeleniumMiddleware': 543,
      'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
    },
    '''
    return build_component_list(
      settings.getwithbase('DOWNLOADER_MIDDLEWARES'))

  # 将所有自定义Middleware中间件的处理函数添加到对应的methods列表中
  def _add_middleware(self, mw):
    if hasattr(mw, 'process_request'):
      self.methods['process_request'].append(mw.process_request)
    if hasattr(mw, 'process_response'):
      self.methods['process_response'].insert(0, mw.process_response)
    if hasattr(mw, 'process_exception'):
      self.methods['process_exception'].insert(0, mw.process_exception)

  # 整个下载流程
  def download(self, download_func, request, spider):
    @defer.inlineCallbacks
    def process_request(request):
      # 处理request请求,依次经过各个自定义Middleware中间件的process_request方法,前面有加入到list中
      for method in self.methods['process_request']:
        response = yield method(request=request, spider=spider)
        assert response is None or isinstance(response, (Response, Request)), \
            'Middleware %s.process_request must return None, Response or Request, got %s' % \
            (six.get_method_self(method).__class__.__name__, response.__class__.__name__)
        # 这是关键地方
        # 如果在某个Middleware中间件的process_request中处理完之后,生成了一个response对象
        # 那么会直接将这个response return 出去,跳出循环,不再处理其他的process_request
        # 之前我们的header,proxy中间件,都只是加个user-agent,加个proxy,并不做任何return值
        # 还需要注意一点:就是这个return的必须是Response对象
        # 后面我们构造的HtmlResponse正是Response的子类对象
        if response:
          defer.returnValue(response)
      # 如果在上面的所有process_request中,都没有返回任何Response对象的话
      # 最后,会将这个加工过的Request送往download_func,进行下载,返回的就是一个Response对象
      # 然后依次经过各个Middleware中间件的process_response方法进行加工,如下
      defer.returnValue((yield download_func(request=request,spider=spider)))

    @defer.inlineCallbacks
    def process_response(response):
      assert response is not None, 'Received None in process_response'
      if isinstance(response, Request):
        defer.returnValue(response)

      for method in self.methods['process_response']:
        response = yield method(request=request, response=response,
                    spider=spider)
        assert isinstance(response, (Response, Request)), \
          'Middleware %s.process_response must return Response or Request, got %s' % \
          (six.get_method_self(method).__class__.__name__, type(response))
        if isinstance(response, Request):
          defer.returnValue(response)
      defer.returnValue(response)

    @defer.inlineCallbacks
    def process_exception(_failure):
      exception = _failure.value
      for method in self.methods['process_exception']:
        response = yield method(request=request, exception=exception,
                    spider=spider)
        assert response is None or isinstance(response, (Response, Request)), \
          'Middleware %s.process_exception must return None, Response or Request, got %s' % \
          (six.get_method_self(method).__class__.__name__, type(response))
        if response:
          defer.returnValue(response)
      defer.returnValue(_failure)

    deferred = mustbe_deferred(process_request, request)
    deferred.addErrback(process_exception)
    deferred.addCallback(process_response)
    return deferred

4. 代码

在settings.py中,配置好selenium参数:

# 文件settings.py中

# ----------- selenium参数配置 -------------
SELENIUM_TIMEOUT = 25      # selenium浏览器的超时时间,单位秒
LOAD_IMAGE = True        # 是否下载图片
WINDOW_HEIGHT = 900       # 浏览器窗口大小
WINDOW_WIDTH = 900

在spider中,生成request时,标记哪些请求需要走selenium下载:

# 文件mySpider.py中
class mySpider(CrawlSpider):
  name = "mySpiderAmazon"
  allowed_domains = ['amazon.com']

  custom_settings = {
    'LOG_LEVEL':'INFO',
    'DOWNLOAD_DELAY': 0,
    'COOKIES_ENABLED': False, # enabled by default
    'DOWNLOADER_MIDDLEWARES': {
      # 代理中间件
      'mySpider.middlewares.ProxiesMiddleware': 400,
      # SeleniumMiddleware 中间件
      'mySpider.middlewares.SeleniumMiddleware': 543,
      # 将scrapy默认的user-agent中间件关闭
      'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
    },

#.....................华丽的分割线.......................
# 生成request时,将是否使用selenium下载的标记,放入到meta中
yield Request(
  url = "https://www.amazon.com/",
  meta = {'usedSelenium': True, 'dont_redirect': True},
  callback = self.parseIndexPage,
  errback = self.error
)

在下载中间件middlewares.py中,使用selenium抓取页面(核心部分

# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from scrapy.http import HtmlResponse
from logging import getLogger
import time

class SeleniumMiddleware():
  # 经常需要在pipeline或者中间件中获取settings的属性,可以通过scrapy.crawler.Crawler.settings属性
  @classmethod
  def from_crawler(cls, crawler):
    # 从settings.py中,提取selenium设置参数,初始化类
    return cls(timeout=crawler.settings.get('SELENIUM_TIMEOUT'),
          isLoadImage=crawler.settings.get('LOAD_IMAGE'),
          windowHeight=crawler.settings.get('WINDOW_HEIGHT'),
          windowWidth=crawler.settings.get('WINDOW_WIDTH')
          )

  def __init__(self, timeout=30, isLoadImage=True, windowHeight=None, windowWidth=None):
    self.logger = getLogger(__name__)
    self.timeout = timeout
    self.isLoadImage = isLoadImage
    # 定义一个属于这个类的browser,防止每次请求页面时,都会打开一个新的chrome浏览器
    # 这样,这个类处理的Request都可以只用这一个browser
    self.browser = webdriver.Chrome()
    if windowHeight and windowWidth:
      self.browser.set_window_size(900, 900)
    self.browser.set_page_load_timeout(self.timeout)    # 页面加载超时时间
    self.wait = WebDriverWait(self.browser, 25)       # 指定元素加载超时时间

    def process_request(self, request, spider):
    '''
    用chrome抓取页面
    :param request: Request请求对象
    :param spider: Spider对象
    :return: HtmlResponse响应
    '''
    # self.logger.debug('chrome is getting page')
    print(f"chrome is getting page")
    # 依靠meta中的标记,来决定是否需要使用selenium来爬取
    usedSelenium = request.meta.get('usedSelenium', False)
    if usedSelenium:
      try:
        self.browser.get(request.url)
        # 搜索框是否出现
        input = self.wait.until(
          EC.presence_of_element_located((By.XPATH, "//div[@class='nav-search-field ']/input"))
        )
        time.sleep(2)
        input.clear()
        input.send_keys("iphone 7s")
        # 敲enter键, 进行搜索
        input.send_keys(Keys.RETURN)
        # 查看搜索结果是否出现
        searchRes = self.wait.until(
          EC.presence_of_element_located((By.XPATH, "//div[@id='resultsCol']"))
        )
      except Exception as e:
        # self.logger.debug(f'chrome getting page error, Exception = {e}')
        print(f"chrome getting page error, Exception = {e}")
        return HtmlResponse(url=request.url, status=500, request=request)
      else:
        time.sleep(3)
        return HtmlResponse(url=request.url,
                  body=self.browser.page_source,
                  request=request,
                  # 最好根据网页的具体编码而定
                  encoding='utf-8',
                  status=200)

5. 执行结果

如何在scrapy中集成selenium爬取网页的方法 

如何在scrapy中集成selenium爬取网页的方法

6. 存在的问题

6.1. Spider关闭了,chrome没有退出。

2018-04-04 09:26:18 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/response_bytes': 2092766,
'downloader/response_count': 2,
'downloader/response_status_count/200': 2,
'finish_reason': 'finished',
'finish_time': datetime.datetime(2018, 4, 4, 1, 26, 16, 763602),
'log_count/INFO': 7,
'request_depth_max': 1,
'response_received_count': 2,
'scheduler/dequeued': 2,
'scheduler/dequeued/memory': 2,
'scheduler/enqueued': 2,
'scheduler/enqueued/memory': 2,
'start_time': datetime.datetime(2018, 4, 4, 1, 25, 48, 301602)}
2018-04-04 09:26:18 [scrapy.core.engine] INFO: Spider closed (finished)

上面,我们是把browser对象放到了Middleware中间件中,只能做process_request和process_response, 没有说在中间件中介绍如何调用scrapy的close方法。

解决方案:利用信号量的方式,当收到spider_closed信号时,调用browser.quit()

6.2. 当一个项目同时启动多个spider,会共用到Middleware中的selenium,不利于并发。

因为用scrapy + selenium的方式,只有部分,甚至是一小部分页面会用到chrome,既然把chrome放到Middleware中有这么多限制,那为什么不能把chrome放到spider里面呢。这样的好处在于:每个spider都有自己的chrome,这样当启动多个spider时,就会有多个chrome,不是所有的spider共用一个chrome,这对我们的并发是有好处的。

解决方案:将chrome的初始化放到spider中,每个spider独占自己的chrome

 7. 改进版代码

在settings.py中,配置好selenium参数:

# 文件settings.py中

# ----------- selenium参数配置 -------------
SELENIUM_TIMEOUT = 25      # selenium浏览器的超时时间,单位秒
LOAD_IMAGE = True        # 是否下载图片
WINDOW_HEIGHT = 900       # 浏览器窗口大小
WINDOW_WIDTH = 900

在spider中,生成request时,标记哪些请求需要走selenium下载:

# 文件mySpider.py中
# selenium相关库
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait

# scrapy 信号相关库
from scrapy.utils.project import get_project_settings
# 下面这种方式,即将废弃,所以不用
# from scrapy.xlib.pydispatch import dispatcher
from scrapy import signals
# scrapy最新采用的方案
from pydispatch import dispatcher

class mySpider(CrawlSpider):
  name = "mySpiderAmazon"
  allowed_domains = ['amazon.com']

  custom_settings = {
    'LOG_LEVEL':'INFO',
    'DOWNLOAD_DELAY': 0,
    'COOKIES_ENABLED': False, # enabled by default
    'DOWNLOADER_MIDDLEWARES': {
      # 代理中间件
      'mySpider.middlewares.ProxiesMiddleware': 400,
      # SeleniumMiddleware 中间件
      'mySpider.middlewares.SeleniumMiddleware': 543,
      # 将scrapy默认的user-agent中间件关闭
      'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
    },

  # 将chrome初始化放到spider中,成为spider中的元素
  def __init__(self, timeout=30, isLoadImage=True, windowHeight=None, windowWidth=None):
    # 从settings.py中获取设置参数
    self.mySetting = get_project_settings()
    self.timeout = self.mySetting['SELENIUM_TIMEOUT']
    self.isLoadImage = self.mySetting['LOAD_IMAGE']
    self.windowHeight = self.mySetting['WINDOW_HEIGHT']
    self.windowWidth = self.mySetting['windowWidth']
    # 初始化chrome对象
    self.browser = webdriver.Chrome()
    if self.windowHeight and self.windowWidth:
      self.browser.set_window_size(900, 900)
    self.browser.set_page_load_timeout(self.timeout)    # 页面加载超时时间
    self.wait = WebDriverWait(self.browser, 25)       # 指定元素加载超时时间
    super(mySpider, self).__init__()
    # 设置信号量,当收到spider_closed信号时,调用mySpiderCloseHandle方法,关闭chrome
    dispatcher.connect(receiver = self.mySpiderCloseHandle,
              signal = signals.spider_closed
              )

  # 信号量处理函数:关闭chrome浏览器
  def mySpiderCloseHandle(self, spider):
    print(f"mySpiderCloseHandle: enter ")
    self.browser.quit()

#.....................华丽的分割线.......................
# 生成request时,将是否使用selenium下载的标记,放入到meta中
yield Request(
  url = "https://www.amazon.com/",
  meta = {'usedSelenium': True, 'dont_redirect': True},
  callback = self.parseIndexPage,
  errback = self.error
)

在下载中间件middlewares.py中,使用selenium抓取页面

# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from scrapy.http import HtmlResponse
from logging import getLogger
import time

class SeleniumMiddleware():
  # Middleware中会传递进来一个spider,这就是我们的spider对象,从中可以获取__init__时的chrome相关元素
  def process_request(self, request, spider):
    '''
    用chrome抓取页面
    :param request: Request请求对象
    :param spider: Spider对象
    :return: HtmlResponse响应
    '''
    print(f"chrome is getting page")
    # 依靠meta中的标记,来决定是否需要使用selenium来爬取
    usedSelenium = request.meta.get('usedSelenium', False)
    if usedSelenium:
      try:
        spider.browser.get(request.url)
        # 搜索框是否出现
        input = spider.wait.until(
          EC.presence_of_element_located((By.XPATH, "//div[@class='nav-search-field ']/input"))
        )
        time.sleep(2)
        input.clear()
        input.send_keys("iphone 7s")
        # 敲enter键, 进行搜索
        input.send_keys(Keys.RETURN)
        # 查看搜索结果是否出现
        searchRes = spider.wait.until(
          EC.presence_of_element_located((By.XPATH, "//div[@id='resultsCol']"))
        )
      except Exception as e:
        print(f"chrome getting page error, Exception = {e}")
        return HtmlResponse(url=request.url, status=500, request=request)
      else:
        time.sleep(3)
        # 页面爬取成功,构造一个成功的Response对象(HtmlResponse是它的子类)
        return HtmlResponse(url=request.url,
                  body=spider.browser.page_source,
                  request=request,
                  # 最好根据网页的具体编码而定
                  encoding='utf-8',
                  status=200)

运行结果(spider结束,执行mySpiderCloseHandle关闭chrome浏览器):

['categorySelectorAmazon1.pipelines.MongoPipeline']
2018-04-04 11:56:21 [scrapy.core.engine] INFO: Spider opened
2018-04-04 11:56:21 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
chrome is getting page
parseProductDetail url = https://www.amazon.com/, status = 200, meta = {'usedSelenium': True, 'dont_redirect': True, 'download_timeout': 25.0, 'proxy': 'http://H37XPSB6V57VU96D:CAB31DAEB9313CE5@proxy.abuyun.com:9020', 'depth': 0}
chrome is getting page
2018-04-04 11:56:54 [scrapy.core.engine] INFO: Closing spider (finished)
mySpiderCloseHandle: enter
2018-04-04 11:56:59 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/response_bytes': 1938619,
'downloader/response_count': 2,
'downloader/response_status_count/200': 2,
'finish_reason': 'finished',
'finish_time': datetime.datetime(2018, 4, 4, 3, 56, 54, 301602),
'log_count/INFO': 7,
'request_depth_max': 1,
'response_received_count': 2,
'scheduler/dequeued': 2,
'scheduler/dequeued/memory': 2,
'scheduler/enqueued': 2,
'scheduler/enqueued/memory': 2,
'start_time': datetime.datetime(2018, 4, 4, 3, 56, 21, 642602)}
2018-04-04 11:56:59 [scrapy.core.engine] INFO: Spider closed (finished)

到此这篇关于如何在scrapy中集成selenium爬取网页的方法的文章就介绍到这了,更多相关scrapy集成selenium爬取网页内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
Python基类函数的重载与调用实例分析
Jan 12 Python
python打开文件并获取文件相关属性的方法
Apr 23 Python
详解Python 2.6 升级至 Python 2.7 的实践心得
Apr 27 Python
Python将list中的string批量转化成int/float的方法
Jun 26 Python
python在html中插入简单的代码并加上时间戳的方法
Oct 16 Python
解决Python正则表达式匹配反斜杠''\''问题
Jul 17 Python
用Python实现二叉树、二叉树非递归遍历及绘制的例子
Aug 09 Python
pandas apply多线程实现代码
Aug 17 Python
Python中免验证跳转到内容页的实例代码
Oct 23 Python
python Polars库的使用简介
Apr 21 Python
Python趣味挑战之给幼儿园弟弟生成1000道算术题
May 28 Python
用PYTHON去计算88键钢琴的琴键频率和音高
Apr 10 Python
Python 实现键盘鼠标按键模拟
Nov 18 #Python
如何向scrapy中的spider传递参数的几种方法
Nov 18 #Python
python更新数据库中某个字段的数据(方法详解)
Nov 18 #Python
Python下载的11种姿势(小结)
Nov 18 #Python
Python监听键盘和鼠标事件的示例代码
Nov 18 #Python
Opencv python 图片生成视频的方法示例
Nov 18 #Python
calendar在python3时间中常用函数举例详解
Nov 18 #Python
You might like
php中serialize序列化与json性能测试的示例分析
2013/04/27 PHP
PHP实现根据银行卡号判断银行
2015/04/29 PHP
简单谈谈php延迟静态绑定
2016/01/26 PHP
php封装的表单验证类完整实例
2016/10/19 PHP
Yii实现微信公众号场景二维码的方法实例
2020/08/30 PHP
Jquery chosen动态设置值实例介绍
2013/08/08 Javascript
Jquery实现自定义tooltip示例代码
2014/02/12 Javascript
js定时器(执行一次、重复执行)
2014/03/07 Javascript
cocos creator Touch事件应用(触控选择多个子节点的实例)
2017/09/10 Javascript
vue 每次渲染完页面后div的滚动条保持在最底部的方法
2018/03/17 Javascript
Nodejs实现图片上传、压缩预览、定时删除功能
2019/10/25 NodeJs
ES6常用小技巧总结【去重、交换、合并、反转、迭代、计算等】
2019/12/21 Javascript
npx create-react-app xxx创建项目报错的解决办法
2020/02/17 Javascript
Taro UI框架开发小程序实现左滑喜欢右滑不喜欢效果的示例代码
2020/05/18 Javascript
JavaScript浅层克隆与深度克隆示例详解
2020/09/01 Javascript
在实例中重学JavaScript事件循环
2020/12/03 Javascript
[01:45]2014DOTA2 TI预选赛预选赛 大神专访第二弹!
2014/05/20 DOTA
利用numpy和pandas处理csv文件中的时间方法
2018/04/19 Python
对python sklearn one-hot编码详解
2018/07/10 Python
Python面向对象程序设计类的封装与继承用法示例
2019/04/12 Python
Python列表解析操作实例总结
2020/02/26 Python
PyCharm Anaconda配置PyQt5开发环境及创建项目的教程详解
2020/03/24 Python
Pyspark读取parquet数据过程解析
2020/03/27 Python
Python魔术方法专题
2020/06/19 Python
vue 中 get / delete 传递数组参数方法
2021/03/23 Vue.js
简历中个人自我评价范文
2013/12/26 职场文书
小学新学期教师寄语
2014/01/18 职场文书
幼儿园教师个人反思
2014/01/30 职场文书
《春天来了》教学反思
2014/04/07 职场文书
秋天的雨教学反思
2014/04/27 职场文书
就业协议书样本
2014/08/20 职场文书
2015年幼儿园元旦亲子活动方案
2014/12/09 职场文书
新店开张宣传语
2015/07/13 职场文书
小学远程教育工作总结
2015/08/13 职场文书
学生病假条怎么写
2015/08/17 职场文书
java executor包参数处理功能 
2022/02/15 Java/Android