1

使用 scrapy 做采集实在是爽,但是遇到网站反爬措施做的比较好的就让人头大了。除了硬着头皮上以外,还可以使用爬虫利器 seleniumselenium 因其良好的模拟能力成为爬虫爱(cai)好(ji)者爱不释手的武器。但是其速度又往往令人感到美中不足,特别是在与 scrapy 集成使用时,严重拖了 scrapy 的后腿,整个采集过程让人看着实在不爽,那么有没有更好的方式来使用呢?答案当然是必须的。

2

twisted 开发者在遇到与 MySQL 数据库交互时,也有同样的问题:如何在异步循环中更好的调用一个IO阻塞的函数?于是他们实现了 adbapi,将阻塞方法放进了线程池中执行。基于此,我们也可以将 selenium 相关的方法放入线程池中执行,这样就可以极大的减少等待的时间。

3

由于 scrapy 是基于 twisted 开发的,因此基于 twisted 线程池实现 selenium 浏览器池,就能很好的与 scrapy 融合在一起了,所以本次就基于 twistedthreadpool 开发,手把手写一个下载中间件,用来实现 scrapyselenium 的优雅配合。

4

首先是对于请求类的定义,我们让 selenium 只接受自定义的请求类调用,考虑到 selenium 中可等待,可执行 JavaScript,因此为其定义了 wait_untilwait_timescript 三个属性,同时考虑到可能会在请求成功后对 webdriver 做自定制的操作,因此还定义了一个 handler 属性,该属性接受一个方法,仅可接受 driverrequestspider 三个参数,分别表示当前浏览器实例、当前请求实例、当前爬虫实例,该方法可以有返回值,当该方法返回一个 RequestResponse 对象时,与在 scrapy 中的下载中间中的 process_request 方法返回值具有同等作用:

  1. import scrapy
  2. class SeleniumRequest(scrapy.Request):
  3. def __init__(self,
  4. url,
  5. callback=None,
  6. wait_until=None,
  7. wait_time=10,
  8. script=None,
  9. handler=None,
  10. **kwargs):
  11. self.wait_until = wait_until
  12. self.wait_time = wait_time
  13. self.script = script
  14. self.handler = handler
  15. super().__init__(url, callback, **kwargs)

5

定义好请求类后,还需要实现浏览器类,用于创建 webdriver 实例,同时做一些规避检测和简单优化的动作,并支持不同的浏览器,鉴于精力有限,这里仅支持 chromefirefox 浏览器:

  1. from scrapy.http import HtmlResponse
  2. from selenium import webdriver
  3. from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
  4. class Browser(object):
  5. """Browser to make drivers"""
  6. # 支持的浏览器名称及对应的类
  7. support_driver_map = {
  8. 'firefox': webdriver.Firefox,
  9. 'chrome': webdriver.Chrome
  10. }
  11. def __init__(self, driver_name='chrome', executable_path=None, options=None, **opt_kw):
  12. assert driver_name in self.support_driver_map, f'{driver_name} not be supported!'
  13. self.driver_name = driver_name
  14. self.executable_path = executable_path
  15. if options is not None:
  16. self.options = options
  17. else:
  18. self.options = make_options(self.driver_name, **opt_kw)
  19. def driver(self):
  20. kwargs = {'executable_path': self.executable_path, 'options': self.options}
  21. # 关闭日志文件,仅适用于windows平台
  22. if self.driver_name == 'firefox':
  23. kwargs['service_log_path'] = 'nul'
  24. driver = self.support_driver_map[self.driver_name](**kwargs)
  25. self.prepare_driver(driver)
  26. return _WebDriver(driver)
  27. def prepare_driver(self, driver):
  28. if isinstance(driver, webdriver.Chrome):
  29. # 移除 `window.navigator.webdriver`.
  30. driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
  31. "source": """
  32. Object.defineProperty(navigator, 'webdriver', {
  33. get: () => undefined
  34. })
  35. """
  36. })
  37. def make_options(driver_name, headless=True, disable_image=True, user_agent=None):
  38. """
  39. params headless: 是否隐藏界面
  40. params disable_image: 是否关闭图像
  41. params user_agent: 浏览器标志
  42. """
  43. if driver_name == 'chrome':
  44. options = webdriver.ChromeOptions()
  45. options.headless = headless
  46. # 关闭 gpu 渲染
  47. options.add_argument('--disable-gpu')
  48. if user_agent:
  49. options.add_argument(f"--user-agent={user_agent}")
  50. if disable_image:
  51. options.add_experimental_option('prefs', {'profile.default_content_setting_values': {'images': 2}})
  52. # 规避检测
  53. options.add_experimental_option('excludeSwitches', ['enable-automation', ])
  54. return options
  55. elif driver_name == 'firefox':
  56. options = webdriver.FirefoxOptions()
  57. options.headless = headless
  58. if disable_image:
  59. options.set_preference('permissions.default.image', 2)
  60. if user_agent:
  61. options.set_preference('general.useragent.override', user_agent)
  62. return options

其中,Browser 类的 driver 方法用于创建 webdriver 实例,注意到其返回的并不是原生的 seleniumwebdriver 实例,而是一个经过自定义的类,因为笔者有意为其实现一个特殊的方法,所以使用了代理类(其方法调用和 selenium 中的 webdriver 并无不同,只是多了一个新的方法),代码如下:

  1. class _WebDriver(object):
  2. def __init__(self, driver: RemoteWebDriver):
  3. self._driver = driver
  4. self._is_idle = False
  5. def __getattr__(self, item):
  6. return getattr(self._driver, item)
  7. def current_response(self, request):
  8. """返回当前页面的 response 对象"""
  9. return HtmlResponse(self.current_url,
  10. body=str.encode(self.page_source),
  11. encoding='utf-8',
  12. request=request)

6

到此,终于到了最重要的一步:基于 selenium 的浏览器池实现,其实也就是进程池,只不过将初始化浏览器以及通过浏览器请求的操作交给了不同的进程而已。鉴于使用下载中间件的方式实现,因此可以将可配置属性放入 scrapy 项目中的settings.py文件中,初始化时候方便直接读取。这里先对可配置字段及其默认值说明:

  1. # 最小 driver 实例数量
  2. SELENIUM_MIN_DRIVERS = 3
  3. # 最大 driver 实例数量
  4. SELENIUM_MAX_DRIVERS = 5
  5. # 是否隐藏界面
  6. SELENIUM_HEADLESS = True
  7. # 是否关闭图像加载
  8. SELENIUM_DISABLE_IMAGE = True
  9. # driver 初始化时的执行路径
  10. SELENIUM_DRIVER_PATH = None
  11. # 浏览器名称
  12. SELENIUM_DRIVER_NAME = 'chrome'
  13. # 浏览器标志
  14. USER_AGENT = ...

接下来,就是中间件代码实现及其相应说明:

  1. import logging
  2. import threading
  3. from scrapy import signals
  4. from scrapy.http import Request, Response
  5. from selenium.webdriver.support.ui import WebDriverWait
  6. from scrapy_ajax_utils.selenium.browser import Browser
  7. from scrapy_ajax_utils.selenium.request import SeleniumRequest
  8. from twisted.internet import threads, reactor
  9. from twisted.python.threadpool import ThreadPool
  10. logger = logging.getLogger(__name__)
  11. class SeleniumDownloaderMiddleware(object):
  12. @classmethod
  13. def from_crawler(cls, crawler):
  14. settings = crawler.settings
  15. min_drivers = settings.get('SELENIUM_MIN_DRIVERS', 3)
  16. max_drivers = settings.get('SELENIUM_MAX_DRIVERS', 5)
  17. # 初始化浏览器
  18. browser = _make_browser_from_settings(settings)
  19. dm = cls(browser, min_drivers, max_drivers)
  20. # 绑定方法用于在爬虫结束后执行
  21. crawler.signals.connect(dm.spider_closed, signal=signals.spider_closed)
  22. return dm
  23. def __init__(self, browser, min_drivers, max_drivers):
  24. self._browser = browser
  25. self._drivers = set() # 存储启动的 driver 实例
  26. self._data = threading.local() # 使用 ThreadLocal 绑定线程与 driver
  27. self._threadpool = ThreadPool(min_drivers, max_drivers) # 创建线程池
  28. def process_request(self, request, spider):
  29. # 过滤非目标请求实例
  30. if not isinstance(request, SeleniumRequest):
  31. return
  32. # 检测线程池是否启动
  33. if not self._threadpool.started:
  34. self._threadpool.start()
  35. # 调用线程池执行浏览器请求
  36. return threads.deferToThreadPool(
  37. reactor, self._threadpool, self.download_by_driver, request, spider
  38. )
  39. def download_by_driver(self, request, spider):
  40. driver = self.get_driver()
  41. driver.get(request.url)
  42. # 等待条件
  43. if request.wait_until:
  44. WebDriverWait(driver, request.wait_time).until(request.wait_until)
  45. # 执行 JavaScript 并将执行结果放入 meta 中
  46. if request.script:
  47. request.meta['js_result'] = driver.execute_script(request.script)
  48. # 调用自定制操作方法并检测返回值
  49. if request.handler:
  50. result = request.handler(driver, request, spider)
  51. if isinstance(result, (Request, Response)):
  52. return result
  53. # 返回当前页面的 response 对象
  54. return driver.current_response(request)
  55. def get_driver(self):
  56. """
  57. 获取当前线程绑定的 driver 对象
  58. 如果没有则创建新的对象
  59. 并绑定到当前线程中
  60. 同时添加到已启动 driver 中
  61. 最后返回
  62. """
  63. try:
  64. driver = self._data.driver
  65. except AttributeError:
  66. driver = self._browser.driver()
  67. self._drivers.add(driver)
  68. self._data.driver = driver
  69. return driver
  70. def spider_closed(self):
  71. """关闭所有启动的 driver 对象,并关闭线程池"""
  72. for driver in self._drivers:
  73. driver.quit()
  74. logger.debug('all webdriver closed.')
  75. self._threadpool.stop()
  76. def _make_browser_from_settings(settings):
  77. headless = settings.getbool('SELENIUM_HEADLESS', True)
  78. disable_image = settings.get('SELENIUM_DISABLE_IMAGE', True)
  79. driver_name = settings.get('SELENIUM_DRIVER_NAME', 'chrome')
  80. executable_path = settings.get('SELENIUM_DRIVER_PATH')
  81. user_agent = settings.get('USER_AGENT')
  82. return Browser(headless=headless,
  83. disable_image=disable_image,
  84. driver_name=driver_name,
  85. executable_path=executable_path,
  86. user_agent=user_agent)

7

嫌代码写着麻烦?没关系,这里有一份已经写好的代码:https://github.com/kingron117/scrapy_ajax_utils

只需要 pip install scrapy-ajax-utils 即可食用~

8

本次代码实现主要参(chao)考(xi)了以下两个项目:

  1. https://github.com/scrapy-plugins/scrapy-headless
  2. https://github.com/clemfromspace/scrapy-selenium

如何优雅的在scrapy中使用selenium —— 在scrapy中实现浏览器池的更多相关文章

  1. 在Scrapy中使用selenium

    在scrapy中使用selenium 在scrapy中需要获取动态加载的数据的时候,可以在下载中间件中使用selenium 编码步骤: 在爬虫文件中导入webdrvier类 在爬虫文件的爬虫类的构造方 ...

  2. Java中通过Selenium WebDriver定位iframe中的元素

    转载请注明出自天外归云的博客园:http://www.cnblogs.com/LanTianYou/ 问题:有一些元素,无论是通过id或是xpath等等,怎么都定位不到. 分析:这很可能是因为你要定位 ...

  3. selenium在scrapy中的使用、UA池、IP池的构建

    selenium在scrapy中的使用流程 重写爬虫文件的构造方法__init__,在该方法中使用selenium实例化一个浏览器对象(因为浏览器对象只需要被实例化一次). 重写爬虫文件的closed ...

  4. Scrapy中集成selenium

    面对众多动态网站比如说淘宝等,一般情况下用selenium最好 那么如何集成selenium到scrapy中呢? 因为每一次request的请求都要经过中间件,所以写在中间件中最为合适 from se ...

  5. selenium在scrapy中的应用

    引入 在通过scrapy框架进行某些网站数据爬取的时候,往往会碰到页面动态数据加载的情况发生,如果直接使用scrapy对其url发请求,是绝对获取不到那部分动态加载出来的数据值.但是通过观察我们会发现 ...

  6. 爬虫开发12.selenium在scrapy中的应用

    selenium在scrapy中的应用阅读量: 370 1 引入 在通过scrapy框架进行某些网站数据爬取的时候,往往会碰到页面动态数据加载的情况发生,如果直接使用scrapy对其url发请求,是绝 ...

  7. scrapy中的selenium

    引入 在通过scrapy框架进行某些网站数据爬取的时候,往往会碰到页面动态数据加载的情况发生,如果直接使用scrapy对其url发请求,是绝对获取不到那部分动态加载出来的数据值.但是通过观察我们会发现 ...

  8. scrapy中间件中使用selenium切换ip

    scrapy抓取一些需要js加载页面时一般要么是通过接口直接获取数据,要么是js加载,但是我通过selenium也可以获取动态页面 但是有个问题,容易给反爬,因为在scrapy中间件mid中使用sel ...

  9. scrapy中使用selenium来爬取页面

    scrapy中使用selenium来爬取页面 from selenium import webdriver from scrapy.http.response.html import HtmlResp ...

随机推荐

  1. Javascript复制内容到剪贴板,解决navigator.clipboard Cannot read property 'writeText' of undefined

    起因 最近帮同事实现了一个小功能--复制文本到剪贴板,主要参考了前端大神阮一峰的博客,根据 navigator.clipboard 返回的 Clipboard 对象的方法 writeText() 写文 ...

  2. Spring Cloud Gateway自定义过滤器实战(观测断路器状态变化)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  3. [hdu7026]Might and Magic

    (以下默认$A_{0},D_{0},P_{0},K_{0}$都为非负整数) 显然存活轮数$S=\lceil\frac{H_{0}}{C_{p}\max(A_{1}-D_{0},1)}\rceil$​​ ...

  4. [luogu7468]愤怒的小N

    定义$count(x)$为$x$二进制下1的个数,答案即$\sum_{0\le x<n,count(x)\equiv 1(mod\ 2)}f(x)$ 考虑预处理出$S_{k,i,p}=\sum_ ...

  5. 学Web前端开发,选择培训学校是关键--青岛思途

    互联网+的提出,催生了Web前端开发行业更大的就业空间,其行业热度也正呈爆炸式增长.专业人才供不应求导致了从业者薪资的居高不下,一般来说Web前端工程师的年薪可达15w以上,工作3~5年后通常可达到1 ...

  6. Codeforces 997E - Good Subsegments(线段树维护最小值个数+历史最小值个数之和)

    Portal 题意: 给出排列 \(p_1,p_2,p_3,\dots,p_n\),定义一个区间 \([l,r]\) 是好的当且仅当 \(p_l,p_{l+1},p_{l+2},\dots,p_r\) ...

  7. CURL常用参数

    1. CURL简介 cURL是一个利用URL语法在命令行下工作的文件传输工具.它支持文件上传和下载,是综合传输工具.cURL就是客户端(client)的URL工具的意思. 2. 常用参数 -k:不校验 ...

  8. 【shell】真正解决syntax error:unexpected end of file?

    今天写了个较长的shell脚本,结构嵌套比较多,最后运行时,出现了syntax error: unexpected end of file的错误. 这个之前碰到过,经常在win系统转移脚本文件到uni ...

  9. Linux非root安装Python3以及解决SSL问题

    说明 接上一篇. [Linux]非root安装Python3及其包管理 上一篇虽然成功安装了Python3及一些常用的模块,但因为一直装不上SSL模块,导致一些包无法安装,尝试了不少方法都失败了(网上 ...

  10. LATEX公式语法

    see how any formula was written in any question or answer, including this one, right-click on the ex ...