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 方法返回值具有同等作用:

import scrapy

class SeleniumRequest(scrapy.Request):

    def __init__(self,
url,
callback=None,
wait_until=None,
wait_time=10,
script=None,
handler=None,
**kwargs):
self.wait_until = wait_until
self.wait_time = wait_time
self.script = script
self.handler = handler
super().__init__(url, callback, **kwargs)

5

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

from scrapy.http import HtmlResponse
from selenium import webdriver
from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver class Browser(object):
"""Browser to make drivers"""
# 支持的浏览器名称及对应的类
support_driver_map = {
'firefox': webdriver.Firefox,
'chrome': webdriver.Chrome
} def __init__(self, driver_name='chrome', executable_path=None, options=None, **opt_kw):
assert driver_name in self.support_driver_map, f'{driver_name} not be supported!'
self.driver_name = driver_name
self.executable_path = executable_path
if options is not None:
self.options = options
else:
self.options = make_options(self.driver_name, **opt_kw) def driver(self):
kwargs = {'executable_path': self.executable_path, 'options': self.options}
# 关闭日志文件,仅适用于windows平台
if self.driver_name == 'firefox':
kwargs['service_log_path'] = 'nul'
driver = self.support_driver_map[self.driver_name](**kwargs)
self.prepare_driver(driver)
return _WebDriver(driver) def prepare_driver(self, driver):
if isinstance(driver, webdriver.Chrome):
# 移除 `window.navigator.webdriver`.
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})
"""
}) def make_options(driver_name, headless=True, disable_image=True, user_agent=None):
"""
params headless: 是否隐藏界面
params disable_image: 是否关闭图像
params user_agent: 浏览器标志
"""
if driver_name == 'chrome':
options = webdriver.ChromeOptions()
options.headless = headless
# 关闭 gpu 渲染
options.add_argument('--disable-gpu')
if user_agent:
options.add_argument(f"--user-agent={user_agent}")
if disable_image:
options.add_experimental_option('prefs', {'profile.default_content_setting_values': {'images': 2}})
# 规避检测
options.add_experimental_option('excludeSwitches', ['enable-automation', ])
return options elif driver_name == 'firefox':
options = webdriver.FirefoxOptions()
options.headless = headless
if disable_image:
options.set_preference('permissions.default.image', 2)
if user_agent:
options.set_preference('general.useragent.override', user_agent)
return options

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

class _WebDriver(object):

    def __init__(self, driver: RemoteWebDriver):
self._driver = driver
self._is_idle = False def __getattr__(self, item):
return getattr(self._driver, item) def current_response(self, request):
"""返回当前页面的 response 对象"""
return HtmlResponse(self.current_url,
body=str.encode(self.page_source),
encoding='utf-8',
request=request)

6

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

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

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

import logging
import threading from scrapy import signals
from scrapy.http import Request, Response
from selenium.webdriver.support.ui import WebDriverWait from scrapy_ajax_utils.selenium.browser import Browser
from scrapy_ajax_utils.selenium.request import SeleniumRequest from twisted.internet import threads, reactor
from twisted.python.threadpool import ThreadPool logger = logging.getLogger(__name__) class SeleniumDownloaderMiddleware(object): @classmethod
def from_crawler(cls, crawler):
settings = crawler.settings
min_drivers = settings.get('SELENIUM_MIN_DRIVERS', 3)
max_drivers = settings.get('SELENIUM_MAX_DRIVERS', 5)
# 初始化浏览器
browser = _make_browser_from_settings(settings)
dm = cls(browser, min_drivers, max_drivers)
# 绑定方法用于在爬虫结束后执行
crawler.signals.connect(dm.spider_closed, signal=signals.spider_closed)
return dm def __init__(self, browser, min_drivers, max_drivers):
self._browser = browser
self._drivers = set() # 存储启动的 driver 实例
self._data = threading.local() # 使用 ThreadLocal 绑定线程与 driver
self._threadpool = ThreadPool(min_drivers, max_drivers) # 创建线程池 def process_request(self, request, spider):
# 过滤非目标请求实例
if not isinstance(request, SeleniumRequest):
return # 检测线程池是否启动
if not self._threadpool.started:
self._threadpool.start() # 调用线程池执行浏览器请求
return threads.deferToThreadPool(
reactor, self._threadpool, self.download_by_driver, request, spider
) def download_by_driver(self, request, spider):
driver = self.get_driver()
driver.get(request.url) # 等待条件
if request.wait_until:
WebDriverWait(driver, request.wait_time).until(request.wait_until) # 执行 JavaScript 并将执行结果放入 meta 中
if request.script:
request.meta['js_result'] = driver.execute_script(request.script) # 调用自定制操作方法并检测返回值
if request.handler:
result = request.handler(driver, request, spider)
if isinstance(result, (Request, Response)):
return result # 返回当前页面的 response 对象
return driver.current_response(request) def get_driver(self):
"""
获取当前线程绑定的 driver 对象
如果没有则创建新的对象
并绑定到当前线程中
同时添加到已启动 driver 中
最后返回
"""
try:
driver = self._data.driver
except AttributeError:
driver = self._browser.driver()
self._drivers.add(driver)
self._data.driver = driver
return driver def spider_closed(self):
"""关闭所有启动的 driver 对象,并关闭线程池"""
for driver in self._drivers:
driver.quit()
logger.debug('all webdriver closed.')
self._threadpool.stop() def _make_browser_from_settings(settings):
headless = settings.getbool('SELENIUM_HEADLESS', True)
disable_image = settings.get('SELENIUM_DISABLE_IMAGE', True)
driver_name = settings.get('SELENIUM_DRIVER_NAME', 'chrome')
executable_path = settings.get('SELENIUM_DRIVER_PATH')
user_agent = settings.get('USER_AGENT')
return Browser(headless=headless,
disable_image=disable_image,
driver_name=driver_name,
executable_path=executable_path,
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. python实现超大图像的二值化方法

    一,分块处理超大图像的二值化问题   (1) 全局阈值处理  (2) 局部阈值 二,空白区域过滤 三,先缩放进行二值化,然后还原大小 np.mean() 返回数组元素的平均值 np.std() 返回数 ...

  2. 自定义 OpenShift s2i 镜像与模板——OracleJDK8

    本文目标 由于 OpenShift 官方提供的镜像与模板(OpenJDK8)不完全满足业务需要: 不包含飞行记录功能.只有 OpenJDK11 以上才被 Oracle 开源 生成堆 dump 很大很慢 ...

  3. [loj3246]Cave Paintings

    题中所给的判定条件似乎比较神奇,那么用严谨的话来说就是对于两个格子(x,y)和(x',y'),如果满足:1.$x\le x'$:2.从(x,y)通过x,x+1,--,n行,允许向四个方向走,不允许经过 ...

  4. Pickle的简单用法

    Python中pickle的用法 pickle存在的意义 在python的文件操作里面,我们常常需要将python容器里面的一些东西把它写成一个二进制文件存放在硬盘里面来永久保存. 在不借助pickl ...

  5. 洛谷 P7116 - [NOIP2020] 微信步数(拉格朗日插值)

    洛谷题面传送门 我竟然独立切掉了这道题!incredible! 纪念我逝去的一上午(NOIP 总时长 4.5h,这题做了我整整 4.5h) 首先讲一下现场我想的 80 分的做法,虽然最后挂成了 65 ...

  6. CF1466G Song of the Sirens

    题目传送门 题意简述:给出 \(n,s_0,t\ (n=|t|)\),定义 \(s_i=s_{i-1}+t_i+s_{i-1}\).多次询问给出 \(k,m\),求 \(m\) 在 \(s_k\) 中 ...

  7. 文件/目录对比:diff命令

    命令格式 diff [参数] [文件1或目录1] [文件2或目录2] Linux diff命令用于比较文件的差异. diff以逐行的方式,比较文本文件的异同处. 如果指定要比较目录,diff会比较目录 ...

  8. python函数理解 json.dump()

    信息来自python说明文档(https://docs.python.org/3/library/json.html) 函数功能 输出一个python对象到文件 函数声明 json.dump(obj, ...

  9. 毕业设计之zabbix=[web检测

    网站对一个公司来说非常重要,里边包含了公司的业务,介绍和订单等相关信息,网站的宕掉了对公司的影响非常重大,所以要很好的对网站的页面进行监控 创建web场景 各部分介绍: Name:唯一的scenari ...

  10. Java 读取txt文件生成Word文档

    本文将以Java程序代码为例介绍如何读取txt文件中的内容,生成Word文档.在编辑代码前,可参考如下代码环境进行配置: IntelliJ IDEA Free Spire.Doc for Java T ...