在scrapy中使用selenium
scrapy是个好工具,selenium也是一个好工具,但是两者一结合,就不那么好了。因为往一个非阻塞程序中塞入一段阻塞的代码,不能不令人抓狂。但即便如此,还是有不少需求需要在scrapy中使用selenium(往往是因为JavaScript搞不定)。既然如此,不妨来试一下怎样更好的利用scraoy特性使用selenium。大概思路如下:
- 编写专属的
SeleniumRequest
类用来封装selenium
的相关操作; - 编写下载中间件,用于启动浏览器,并根据
SeleniumReuqest
的相关属性进行进一步操作。
OK,思路很清晰,接下来就撸起袖子干吧。
编写SeleniumRequest
毫无疑问这个类要继承自Scrapy.Reuqest
,同时我们希望这个类能保存一些属性用于对浏览器的操作。大概如下:
- 首先是
wait_until
,用来保存浏览器等待到我们想要的条件加载出来为止; -
script
,用来保存js
脚本,用于在加载后执行该脚本; -
handler
,该属性为一个函数,接收一个driver
参数,当网页加载完成后调用它。
代码如下:
class SeleniumRequest(scrapy.Request):
"""Selenium Request
:param wait_until: 等待条件
结构: {by: condition}
其中 by 的可指定类型可查看selenium.webdriver.common.by.By
如: By.ID, By.XPATH 等(仅支持指定条件出现)
:type wait_until: dict
:param wait_time: 等待时间
:type wait_time: int
:param script: 需要执行的js脚本
执行的结果会存储到 meta 中,字段为 js_result
:param handler: 处理driver实例的函数
该函数不需要返回值
"""
def __init__(self, url, callback=None,
wait_until=None, wait_time=None,
script=None, handler=None, **kwargs):
self.wait_until = wait_until
self.script = script
self.wait_time = wait_time
self.handler = handler
super().__init__(url, callback, **kwargs)
到此请求类就写完了,接下来开始写下载中间件。
编写下载中间件
下载中间件负责接收SeleniumReuqest
并实际调用浏览器和操作浏览器,最后将浏览器获取到的网页源码封装为HtmlResponse
返回。因此它要做的事相对多一点。下面一步步来写:
- 第一步还是要先定义一下类,构造函数中我们需要一个项目设置实例,因为我们要从配置文件中获取
Webdriver
的启动路径和其它设置信息(规定它必须被配置在scrapy项目的配置文件中,以保持使用上的统一),需要的设置分别为SELENIUM_DRIVER_PATH
和SELENIUM_HEADLESS
,分别表示路径和是否显示浏览器界面。
# 引入下面所有代码需要的模块和方法
import logging
from scrapy import signals
from scrapy.http import HtmlResponse
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
logger = logging.getLogger(__name__)
class SeleniumDownloadMiddleWare(object):
def __init__(self, settings):
driver_path = settings['SELENIUM_DRIVER_PATH']
headless = settings.getbool('SELENIUM_HEADLESS', True)
# 目前就只支持 Chrome 了
options = webdriver.ChromeOptions()
options.headless = headless
# User-Agent与项目配置保持一致
# 否则可能会导致在某些根据该请求头设定cookies的网站上出现意想不到的情况
ua = settings['DEFAULT_REQUEST_HEADERS']['User-Agent']
options.add_argument(f'user-agent={ua}')
self._options = options
self._driver_path = driver_path
self._driver = None
- 接下来定义类方法
from_crawler
用来实例化类。在这里,还要绑定一个爬虫结束的信号,以保证当爬虫结束时测试浏览器被正常关闭。
@classmethod
def from_crawler(cls, crawler):
dm = cls(crawler.settings)
crawler.signals.connect(dm.close, signal=signals.spider_closed)
return dm
- 于是马上就轮到
close
方法了:
def closed(self):
if self._driver is not None:
self._driver.quit()
logger.debug('Selenium closed')
- 写一个
driver
属性方便调用:
@property
def driver(self):
if self._driver is None:
self._driver = webdriver.Chrome(
executable_path=self._driver_path, options=self._options
)
return self._driver
- 终于来到了最后的环节,当然就是写一个
process_request
方法了,我们将通过该方法处理SeleniumRequest
:
def process_request(self, request, spider):
if not isinstance(request, SeleniumRequest):
return
self.driver.get(request.url)
# 处理等待条件
if request.wait_until:
for k, v in request.wait_until.items():
condition = EC.presence_of_element_located((k, v))
WebDriverWait(self.driver, request.wait_time).until(
condition
)
# 处理js脚本
if request.script:
result = self.driver.execute_script(request.script)
if result is not None:
request.meta['js_result'] = result
# 调用处理函数
if request.handler is not None:
request.handler(self.driver)
# 传递Cookies
for cookie_name, cookie_value in request.cookies.items():
self.driver.add_cookie(
{
'name': cookie_name,
'value': cookie_value
}
)
request.cookies = self.driver.get_cookies()
request.meta['browser'] = self.driver
# 返回 Response对象
body = str.encode(self.driver.page_source)
return HtmlResponse(
self.driver.current_url,
body=body,
encoding='utf-8',
request=request
)
到此就写完了,接下来在项目的配置中配置该中间件就可以使用了。完整代码如下:
import logging
from scrapy import signals
from scrapy.http import HtmlResponse
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 这里需要修改为SeleniumRequest的定义处
from utils.selenium import SeleniumRequest
logger = logging.getLogger(__name__)
class SeleniumDownloadMiddleWare(object):
def __init__(self, settings):
driver_path = settings['SELENIUM_DRIVER_PATH']
headless = settings.getbool('SELENIUM_HEADLESS', True)
ua = settings['DEFAULT_REQUEST_HEADERS']['User-Agent']
options = webdriver.ChromeOptions()
options.headless = headless
options.add_argument(f'user-agent={ua}')
self._options = options
self._driver_path = driver_path
self._driver = None
@property
def driver(self):
if self._driver is None:
self._driver = webdriver.Chrome(
executable_path=self._driver_path, options=self._options
)
return self._driver
@classmethod
def from_crawler(cls, crawler):
dm = cls(crawler.settings)
crawler.signals.connect(dm.close, signal=signals.spider_closed)
return dm
def process_request(self, request, spider):
if not isinstance(request, SeleniumRequest):
return
self.driver.get(request.url)
# 处理等待条件
if request.wait_until:
for k, v in request.wait_until.items():
condition = EC.presence_of_element_located((k, v))
WebDriverWait(self.driver, request.wait_time).until(
condition
)
# 处理js脚本
if request.script:
result = self.driver.execute_script(request.script)
if result is not None:
request.meta['js_result'] = result
# 调用处理函数
if request.handler is not None:
request.handler(self.driver)
# 传递Cookies
for cookie_name, cookie_value in request.cookies.items():
self.driver.add_cookie(
{
'name': cookie_name,
'value': cookie_value
}
)
request.cookies = self.driver.get_cookies()
request.meta['browser'] = self.driver
# 返回 Response对象
body = str.encode(self.driver.page_source)
return HtmlResponse(
self.driver.current_url,
body=body,
encoding='utf-8',
request=request
)
def close(self):
if self._driver is not None:
self._driver.quit()
logger.debug('Selenium closed')
网友评论