Scrapy對接Selenium

Scrapy抓取頁面的方式和Requests庫類似,都是直接模擬HTTP請求,因此如果遇到JavaScript渲染的頁面Scrapy同樣是無法抓取的,而在前文中我們抓取JavaScript渲染的頁面有兩種方式,一種是分析Ajax請求,找到其對應的介面抓取,Scrapy中同樣可以用此種方式抓取;另一種是直接用Selenium或Splash模擬瀏覽器進行抓取,這種方式我們不需要關心頁面後台發生了怎樣的請求,也不需要分析渲染過程,我們只需要關心頁面最終結果即可,可見即可爬,所以如果在Scrapy中可以對接Selenium話就可以處理任何網站的抓取了。

本節我們來看一下Scrapy框架中如何對接Selenium,這次我們依然是抓取淘寶商品信息,抓取邏輯和前文中用Selenium抓取淘寶商品一節完全相同。

首先新建項目,名稱叫做scrapyseleniumtest,命令如下:

scrapy startproject scrapyseleniumtestn

隨後新建一個Spider,命令如下:

scrapy genspider taobao www.taobao.comn

接下來把ROBOTSTXT_OBEY修改一下,修改為False。

ROBOTSTXT_OBEY = Falsen

接下來首先定義Item對象,叫做ProductItem,代碼如下:

from scrapy import Item, Fieldnnclass ProductItem(Item):nn collection = productsn image = Field()n price = Field()n deal = Field()n title = Field()n shop = Field()n location = Field()n

在這裡我們定義了6個Field,也就是6個欄位,跟之前的案例完全相同,然後定義了一個collection屬性,即此Item保存的MongoDB的Collection名稱。

接下來我們初步實現Spider的start_requests()方法,實現如下:

from scrapy import Request, Spidernfrom urllib.parse import quotenfrom scrapyseleniumtest.items import ProductItemnnclass TaobaoSpider(Spider):n name = taobaon allowed_domains = [www.taobao.com]n base_url = https://s.taobao.com/search?q=nn def start_requests(self):n for keyword in self.settings.get(KEYWORDS):n for page in range(1, self.settings.get(MAX_PAGE) + 1):n url = self.base_url + quote(keyword)n yield Request(url=url, callback=self.parse, meta={page: page}, dont_filter=True)n

首先我們定義了一個base_url,即商品列表的URL,其後拼接一個搜索關鍵字就是該關鍵字在淘寶的搜索結果商品列表頁面。

在這裡關鍵字我們用KEYWORDS標識,定義為一個列表,最大翻頁頁碼用MAX_PAGE表示,統一定義在setttings.py裡面,定義如下:

KEYWORDS = [iPad]nMAX_PAGE = 100n

在start_requests()方法里我們首先遍歷了關鍵字,隨後遍歷了分頁頁碼,構造Request並生成,由於每次搜索的URL是相同的,所以在這裡分頁頁碼我們用meta參數來傳遞,同時設置dont_filter不去重,這樣爬蟲啟動的時候就會生成每個關鍵字對應的商品列表的每一頁的請求了。

接下來我們就需要處理這些請求的抓取了,這次抓取不同,我們要對接Selenium進行抓取,在這裡採用Downloader Middleware來實現,在Middleware裡面的process_request()方法裡面對每個抓取請求進行處理,啟動瀏覽器並進行頁面渲染,再將渲染後的結果構造一個HtmlResponse返回即可。

代碼實現如下:

from selenium import webdrivernfrom selenium.common.exceptions import TimeoutExceptionnfrom selenium.webdriver.common.by import Bynfrom selenium.webdriver.support.ui import WebDriverWaitnfrom selenium.webdriver.support import expected_conditions as ECnfrom scrapy.http import HtmlResponsenfrom logging import getLoggernnclass SeleniumMiddleware():n def __init__(self, timeout=None, service_args=[]):n self.logger = getLogger(__name__)n self.timeout = timeoutn self.browser = webdriver.PhantomJS(service_args=service_args)n self.browser.set_window_size(1400, 700)n self.browser.set_page_load_timeout(self.timeout)n self.wait = WebDriverWait(self.browser, self.timeout)nn def __del__(self):n self.browser.close()nn def process_request(self, request, spider):n """n 用PhantomJS抓取頁面n :param request: Request對象n :param spider: Spider對象n :return: HtmlResponsen """n self.logger.debug(PhantomJS is Starting)n page = request.meta.get(page, 1)n try:n self.browser.get(request.url)n if page > 1:n input = self.wait.until(n EC.presence_of_element_located((By.CSS_SELECTOR, #mainsrp-pager div.form > input)))n submit = self.wait.until(n EC.element_to_be_clickable((By.CSS_SELECTOR, #mainsrp-pager div.form > span.btn.J_Submit)))n input.clear()n input.send_keys(page)n submit.click()n self.wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, #mainsrp-pager li.item.active > span), str(page)))n self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, .m-itemlist .items .item)))n return HtmlResponse(url=request.url, body=self.browser.page_source, request=request, encoding=utf-8, status=200)n except TimeoutException:n return HtmlResponse(url=request.url, status=500, request=request)nn @classmethodn def from_crawler(cls, crawler):n return cls(timeout=crawler.settings.get(SELENIUM_TIMEOUT),n service_args=crawler.settings.get(PHANTOMJS_SERVICE_ARGS))n

首先我們在init()裡面對一些對象進行初始化,包括PhantomJS、WebDriverWait等對象,同時設置了頁面大小和頁面載入超時時間,隨後在process_request()方法中我們首先通過Request的meta屬性獲取當前需要爬取的頁碼,然後調用PhantomJS對象的get()方法訪問Request的對應的URL,這也就相當於從Request對象裡面獲取了請求鏈接然後再用PhantomJS去載入,而不再使用Scrapy里的Downloader。隨後的處理等待和翻頁的方法在此不再贅述,和前文的原理完全相同。最後等待頁面載入完成之後,我們調用PhantomJS的page_source屬性即可獲取當前頁面的源代碼,然後用它來直接構造了一個HtmlResponse對象並返回,構造它的時候需要傳入多個參數,如url、body等,這些參數實際上就是它的一些基礎屬性,可以查看官方文檔看下它的結構:doc.scrapy.org/en/lates,這樣我們就成功利用PhantomJS來代替Scrapy完成了頁面的載入,最後將Response即可。

這裡可能我們有人可能會納悶了,為什麼通過實現這麼一個Downloader Middleware就可以了呢?之前的Request對象怎麼辦?Scrapy不再處理了嗎?Response返回後又傳遞給了誰來處理?

是的,Request對象到這裡就不會再處理了,也不會再像以前一樣交給Downloader下載了,Response會直接傳給Spider進行解析。

這究竟是為什麼?這時我們需要回顧一下Downloader Middleware的process_request()方法的處理邏輯,在前面我們也提到過,內容如下:

當process_request()方法返回Response對象的時候,接下來更低優先順序的Downloader Middleware的process_request()和process_exception()方法就不會被繼續調用了,轉而依次開始執行每個Downloader Middleware的process_response()方法,調用完畢之後直接將Response對象發送給Spider來處理。

在這裡我們直接返回了一個HtmlResponse對象,它是Response的子類,同樣滿足此條件,返回之後便會順次調用每個Downloader Middleware的process_response()方法,而在process_response()中我們沒有對其做特殊處理,接著他就會被發送給Spider,傳給Request的回調函數進行解析。

到現在我們應該就能了解Downloader Middleware實現Selenium對接的原理了。

在settings.py裡面開啟它的調用:

DOWNLOADER_MIDDLEWARES = {n scrapyseleniumtest.middlewares.SeleniumMiddleware: 543,n}n

接下來Response對象就會回傳給Spider內的回調函數進行解析了,所以下一步我們就實現其回調函數,對網頁來進行解析,代碼如下:

def parse(self, response):n products = response.xpath(n //div[@id="mainsrp-itemlist"]//div[@class="items"][1]//div[contains(@class, "item")])n for product in products:n item = ProductItem()n item[price] = .join(product.xpath(.//div[contains(@class, "price")]//text()).extract()).strip()n item[title] = .join(product.xpath(.//div[contains(@class, "title")]//text()).extract()).strip()n item[shop] = .join(product.xpath(.//div[contains(@class, "shop")]//text()).extract()).strip()n item[image] = .join(product.xpath(.//div[@class="pic"]//img[contains(@class, "img")]/@data-src).extract()).strip()n item[deal] = product.xpath(.//div[contains(@class, "deal-cnt")]//text()).extract_first()n item[location] = product.xpath(.//div[contains(@class, "location")]//text()).extract_first()n yield itemn

在這裡我們使用XPath進行解析,調用response變數的xpath()方法即可,首先我們傳遞了選取所有商品對應的XPath,可以匹配到所有的商品,隨後對結果進行遍歷,依次選取每個商品的名稱、價格、圖片等內容,構造一個ProductItem對象,然後返回即可。

最後我們再實現一個Item Pipeline,將結果保存到MongoDB,實現如下:

import pymongonnclass MongoPipeline(object):n def __init__(self, mongo_uri, mongo_db):n self.mongo_uri = mongo_urin self.mongo_db = mongo_dbnn @classmethodn def from_crawler(cls, crawler):n return cls(mongo_uri=crawler.settings.get(MONGO_URI), mongo_db=crawler.settings.get(MONGO_DB))nn def open_spider(self, spider):n self.client = pymongo.MongoClient(self.mongo_uri)n self.db = self.client[self.mongo_db]nn def process_item(self, item, spider):n self.db[item.collection].insert(dict(item))n return itemnn def close_spider(self, spider):n self.client.close()n

此實現和前文中存儲到MongoDB的方法完全一致,原理不再贅述,記得在settings.py中開啟它的調用:

ITEM_PIPELINES = {n scrapyseleniumtest.pipelines.MongoPipeline: 300,n}n

其中MONGO_URI和MONGO_DB的定義如下:

MONGO_URI = localhostnMONGO_DB = taobaon

這樣整個項目就完成了,執行如下命令啟動抓取即可:

scrapy crawl taobaon

運行結果如下:

再查看一下MongoDB,結果如下:

這樣我們便成功在Scrapy中對接Selenium並實現了淘寶商品的抓取,本節代碼:github.com/Python3WebSp

作者:崔慶才 Python愛好者社區專欄作者 授權原創發布,請勿轉載,謝謝。

出處:Scrapy對接Selenium

配套視頻教程:Python3爬蟲三大案例實戰分享:貓眼電影、今日頭條街拍美圖、淘寶美食 Python3爬蟲三大案例實戰分享

公眾號:Python愛好者社區(微信ID:python_shequ)

小編個人微信:tsdatajob ,來不急回復的,可以加小編微信溝通,謝謝。

推薦閱讀:

Python筆記 · 一個多線程知乎用戶爬蟲的實現
Python從零開始系列連載(26)——Python特色數據類型(函數)(上)
Python數據分析及可視化實例之Pandas函數速查表
Python編程(bbb四):兩個實用的Python的裝飾器
用Python寫個彈球的遊戲

TAG:Python | scrapy | Selenium |