使用 scrapy
作採集實在是爽,可是遇到網站反爬措施作的比較好的就讓人頭大了。除了硬着頭皮上之外,還能夠使用爬蟲利器 selenium
,selenium
因其良好的模擬能力成爲爬蟲愛(cai)好(ji)者愛不釋手的武器。可是其速度又每每使人感到美中不足,特別是在與 scrapy
集成使用時,嚴重拖了 scrapy
的後腿,整個採集過程讓人看着實在不爽,那麼有沒有更好的方式來使用呢?答案固然是必須的。python
twisted
開發者在遇到與 MySQL
數據庫交互時,也有一樣的問題:如何在異步循環中更好的調用一個IO阻塞的函數?因而他們實現了 adbapi
,將阻塞方法放進了線程池中執行。基於此,咱們也能夠將 selenium
相關的方法放入線程池中執行,這樣就能夠極大的減小等待的時間。react
因爲 scrapy
是基於 twisted
開發的,所以基於 twisted
線程池實現 selenium
瀏覽器池,就能很好的與 scrapy
融合在一塊兒了,因此本次就基於 twisted
的 threadpool
開發,手把手寫一個下載中間件,用來實現 scrapy
與 selenium
的優雅配合。git
首先是對於請求類的定義,咱們讓 selenium
只接受自定義的請求類調用,考慮到 selenium
中可等待,可執行 JavaScript
,所以爲其定義了 wait_until
、wait_time
、script
三個屬性,同時考慮到可能會在請求成功後對 webdriver
作自定製的操做,所以還定義了一個 handler
屬性,該屬性接受一個方法,僅可接受 driver
、request
、spider
三個參數,分別表示當前瀏覽器實例、當前請求實例、當前爬蟲實例,該方法能夠有返回值,當該方法返回一個 Request
或 Response
對象時,與在 scrapy
中的下載中間中的 process_request
方法返回值具備同等做用:github
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)
定義好請求類後,還須要實現瀏覽器類,用於建立 webdriver
實例,同時作一些規避檢測和簡單優化的動做,並支持不一樣的瀏覽器,鑑於精力有限,這裏僅支持 chrome
和 firefox
瀏覽器:web
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
實例,注意到其返回的並非原生的 selenium
中 webdriver
實例,而是一個通過自定義的類,由於筆者有意爲其實現一個特殊的方法,因此使用了代理類(其方法調用和 selenium
中的 webdriver
並沒有不一樣,只是多了一個新的方法),代碼以下:ajax
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)
到此,終於到了最重要的一步:基於 selenium
的瀏覽器池實現,其實也就是進程池,只不過將初始化瀏覽器以及經過瀏覽器請求的操做交給了不一樣的進程而已。鑑於使用下載中間件的方式實現,所以能夠將可配置屬性放入 scrapy
項目中的settings.py
文件中,初始化時候方便直接讀取。這裏先對可配置字段及其默認值說明:chrome
# 最小 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)
嫌代碼寫着麻煩?不要緊,這裏有一份已經寫好的代碼:https://github.com/kingron117/scrapy_ajax_utils
只須要 pip install scrapy-ajax-utils
便可食用~windows
本次代碼實現主要參(chao)考(xi)瞭如下兩個項目:api