Python3網(wǎng)絡爬蟲開發(fā)實戰(zhàn) Scrapy 對接 Selenium

13.8 Scrapy 對接 Selenium

Scrapy 抓取頁面的方式和 requests 庫類似也祠,都是直接模擬 HTTP 請求,而 Scrapy 也不能抓取 JavaScript 動態(tài)渲染的頁面愿棋。在前文中抓取 JavaScript 渲染的頁面有兩種方式科展。一種是分析 Ajax 請求,找到其對應的接口抓取初斑,Scrapy 同樣可以用此種方式抓取辛润。另一種是直接用 Selenium 或 Splash 模擬瀏覽器進行抓取膨处,我們不需要關心頁面后臺發(fā)生的請求见秤,也不需要分析渲染過程,只需要關心頁面最終結果即可真椿,可見即可爬鹃答。那么,如果 Scrapy 可以對接 Selenium突硝,那 Scrapy 就可以處理任何網(wǎng)站的抓取了测摔。

1. 本節(jié)目標

本節(jié)我們來看看 Scrapy 框架如何對接 Selenium,以 PhantomJS 進行演示。我們依然抓取淘寶商品信息,抓取邏輯和前文中用 Selenium 抓取淘寶商品完全相同犹菱。

2. 準備工作

請確保 PhantomJS 和 MongoDB 已經(jīng)安裝好并可以正常運行也糊,安裝好 Scrapy、Selenium雕凹、PyMongo 庫,安裝方式可以參考第 1 章的安裝說明。

3. 新建項目

首先新建項目紊服,名為 scrapyseleniumtest,命令如下所示:

scrapy startproject scrapyseleniumtest

新建一個 Spider胸竞,命令如下所示:

scrapy genspider taobao www.taobao.com

修改 ROBOTSTXT_OBEY 為 False欺嗤,如下所示:

ROBOTSTXT_OBEY = False

4. 定義 Item

首先定義 Item 對象,名為 ProductItem卫枝,代碼如下所示:

from scrapy import Item, Field

class ProductItem(Item):

collection  =  'products'

image  =  Field()

price  =  Field()

deal  =  Field()

title  =  Field()

shop  =  Field()

location  =  Field()

這里我們定義了 6 個 Field煎饼,也就是 6 個字段,跟之前的案例完全相同校赤。然后定義了一個 collection 屬性吆玖,即此 Item 保存到 MongoDB 的 Collection 名稱。

初步實現(xiàn) Spider 的 start_requests() 方法痒谴,如下所示:

from scrapy import Request, Spider

from urllib.parse import quote

from scrapyseleniumtest.items import ProductItem

class TaobaoSpider(Spider):

name  =  'taobao'

allowed_domains  =  ['www.taobao.com']

base_url  =  'https://s.taobao.com/search?q='

def start_requests(self):

    for  keyword in  self.settings.get('KEYWORDS'):

        for  page in  range(1,  self.settings.get('MAX_PAGE')  +  1):

            url  =  self.base_url  +  quote(keyword)

            yield Request(url=url,  callback=self.parse,  meta={'page':  page},  dont_filter=True)

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

關鍵字用 KEYWORDS 標識意鲸,定義為一個列表。最大翻頁頁碼用 MAX_PAGE 表示。它們統(tǒng)一定義在 setttings.py 里面怎顾,如下所示:

KEYWORDS = ['iPad']

MAX_PAGE = 100

在 start_requests() 方法里读慎,我們首先遍歷了關鍵字,遍歷了分頁頁碼槐雾,構造并生成 Request夭委。由于每次搜索的 URL 是相同的,所以分頁頁碼用 meta 參數(shù)來傳遞募强,同時設置 dont_filter 不去重株灸。這樣爬蟲啟動的時候,就會生成每個關鍵字對應的商品列表的每一頁的請求了擎值。

5. 對接 Selenium

接下來我們需要處理這些請求的抓取慌烧。這次我們對接 Selenium 進行抓取,采用 Downloader Middleware 來實現(xiàn)鸠儿。在 Middleware 里面的 process_request() 方法里對每個抓取請求進行處理屹蚊,啟動瀏覽器并進行頁面渲染,再將渲染后的結果構造一個 HtmlResponse 對象返回进每。代碼實現(xiàn)如下所示:

from selenium import webdriver

from selenium.common.exceptions import TimeoutException

from selenium.webdriver.common.by import By

from selenium.webdriver.support.ui import WebDriverWait

from selenium.webdriver.support import expected_conditions as EC

from scrapy.http import HtmlResponse

from logging import getLogger

class SeleniumMiddleware():

def __init__(self,  timeout=None,  service_args=[]):

    self.logger  =  getLogger(__name__)

    self.timeout  =  timeout

    self.browser  =  webdriver.PhantomJS(service_args=service_args)

    self.browser.set_window_size(1400,  700)

    self.browser.set_page_load_timeout(self.timeout)

    self.wait  =  WebDriverWait(self.browser,  self.timeout)

def __del__(self):

    self.browser.close()

def process_request(self,  request,  spider):

    """

    用 PhantomJS 抓取頁面

    :param request: Request 對象

    :param spider: Spider 對象

    :return: HtmlResponse

    """

    self.logger.debug('PhantomJS is Starting')

    page  =  request.meta.get('page',  1)

    try:

        self.browser.get(request.url)

        if  page  >  1:

            input  =  self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR,  '#mainsrp-pager div.form> input')))

            submit  =  self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,  '#mainsrp-pager div.form> span.btn.J_Submit')))

            input.clear()

            input.send_keys(page)

            submit.click()

        self.wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR,  '#mainsrp-pager li.item.active> span'),  str(page)))

        self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR,  '.m-itemlist .items .item')))

        return  HtmlResponse(url=request.url,  body=self.browser.page_source,  request=request,  encoding='utf-8',  status=200)

    except TimeoutException:

        return  HtmlResponse(url=request.url,  status=500,  request=request)

@classmethod

def from_crawler(cls,  crawler):

    return  cls(timeout=crawler.settings.get('SELENIUM_TIMEOUT'),

service_args=crawler.settings.get('PHANTOMJS_SERVICE_ARGS'))

|

首先我們在 init() 里對一些對象進行初始化汹粤,包括 PhantomJS、WebDriverWait 等對象田晚,同時設置頁面大小和頁面加載超時時間嘱兼。在 process_request() 方法中,我們通過 Request 的 meta 屬性獲取當前需要爬取的頁碼肉瓦,調用 PhantomJS 對象的 get() 方法訪問 Request 的對應的 URL遭京。這就相當于從 Request 對象里獲取請求鏈接,然后再用 PhantomJS 加載泞莉,而不再使用 Scrapy 里的 Downloader哪雕。

隨后的處理等待和翻頁的方法在此不再贅述,和前文的原理完全相同鲫趁。最后斯嚎,頁面加載完成之后,我們調用 PhantomJS 的 page_source 屬性即可獲取當前頁面的源代碼挨厚,然后用它來直接構造并返回一個 HtmlResponse 對象堡僻。構造這個對象的時候需要傳入多個參數(shù),如 url疫剃、body 等钉疫,這些參數(shù)實際上就是它的基礎屬性〕布郏可以在官方文檔查看 HtmlResponse 對象的結構:https://doc.scrapy.org/en/latest/topics/request-response.html牲阁,這樣我們就成功利用 PhantomJS 來代替 Scrapy 完成了頁面的加載固阁,最后將 Response 返回即可。

有人可能會納悶:為什么實現(xiàn)這么一個 Downloader Middleware 就可以了城菊?之前的 Request 對象怎么辦备燃?Scrapy 不再處理了嗎?Response 返回后又傳遞給了誰凌唬?

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

我們需要回顧一下 Downloader Middleware 的 process_request() 方法的處理邏輯,內容如下所示:

當 process_request() 方法返回 Response 對象的時候霎挟,更低優(yōu)先級的 Downloader Middleware 的 process_request() 和 process_exception() 方法就不會被繼續(xù)調用了窝剖,轉而開始執(zhí)行每個 Downloader Middleware 的 process_response() 方法,調用完畢之后直接將 Response 對象發(fā)送給 Spider 來處理酥夭。

這里直接返回了一個 HtmlResponse 對象,它是 Response 的子類脊奋,返回之后便順次調用每個 Downloader Middleware 的 process_response() 方法熬北。而在 process_response() 中我們沒有對其做特殊處理,它會被發(fā)送給 Spider诚隙,傳給 Request 的回調函數(shù)進行解析讶隐。

到現(xiàn)在,我們應該能了解 Downloader Middleware 實現(xiàn) Selenium 對接的原理了久又。

在 settings.py 里巫延,我們設置調用剛才定義的 SeleniumMiddleware、設置等待超時變量 SELENIUM_TIMEOUT地消、設置 PhantomJS 配置參數(shù) PHANTOMJS_SERVICE_ARGS炉峰,如下所示:

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

6. 解析頁面

Response 對象就會回傳給 Spider 內的回調函數(shù)進行解析。所以下一步我們就實現(xiàn)其回調函數(shù)脉执,對網(wǎng)頁來進行解析疼阔,代碼如下所示:

def parse(self, response):

products  =  response.xpath('//div[@id="mainsrp-itemlist"]//div[@class="items"][1]//div[contains(@class, "item")]')

for  product in  products:

    item  =  ProductItem()

    item['price']  =  ''.join(product.xpath('.//div[contains(@class, "price")]//text()').extract()).strip()

    item['title']  =  ''.join(product.xpath('.//div[contains(@class, "title")]//text()').extract()).strip()

    item['shop']  =  ''.join(product.xpath('.//div[contains(@class, "shop")]//text()').extract()).strip()

    item['image']  =  ''.join(product.xpath('.//div[@class="pic"]//img[contains(@class, "img")]/@data-src').extract()).strip()

    item['deal']  =  product.xpath('.//div[contains(@class, "deal-cnt")]//text()').extract_first()

    item['location']  =  product.xpath('.//div[contains(@class, "location")]//text()').extract_first()

    yield item

在這里我們使用 XPath 進行解析,調用 response 變量的 xpath() 方法即可半夷。首先我們傳遞選取所有商品對應的 XPath婆廊,可以匹配所有商品,隨后對結果進行遍歷巫橄,依次選取每個商品的名稱淘邻、價格、圖片等內容湘换,構造并返回一個 ProductItem 對象宾舅。

7. 存儲結果

最后我們實現(xiàn)一個 Item Pipeline敬尺,將結果保存到 MongoDB,如下所示:

import pymongo

class MongoPipeline(object):

def __init__(self,  mongo_uri,  mongo_db):

    self.mongo_uri  =  mongo_uri

    self.mongo_db  =  mongo_db

@classmethod

def from_crawler(cls,  crawler):

    return  cls(mongo_uri=crawler.settings.get('MONGO_URI'),  mongo_db=crawler.settings.get('MONGO_DB'))

def open_spider(self,  spider):

    self.client  =  pymongo.MongoClient(self.mongo_uri)

    self.db  =  self.client[self.mongo_db]

def process_item(self,  item,  spider):

    self.db[item.collection].insert(dict(item))

    return  item

def close_spider(self,  spider):

    self.client.close()

此實現(xiàn)和前文中存儲到 MongoDB 的方法完全一致贴浙,原理不再贅述砂吞。記得在 settings.py 中開啟它的調用,如下所示:

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

其中崎溃,MONGO_URI 和 MONGO_DB 的定義如下所示:

MONGO_URI = 'localhost'

MONGO_DB = 'taobao'

8. 運行

整個項目就完成了蜻直,執(zhí)行如下命令啟動抓取即可:

scrapy crawl taobao

運行結果如圖 13-13 所示:

image

圖 13-13 運行結果

再查看一下 MongoDB,結果如圖 13-14 所示:

image

圖 13-14 MongoDB 結果

這樣我們便成功在 Scrapy 中對接 Selenium 并實現(xiàn)了淘寶商品的抓取袁串。

9. 本節(jié)代碼

本節(jié)代碼地址為:https://github.com/Python3WebSpider/ScrapySeleniumTest概而。

10. 結語

我們通過改寫 Downloader Middleware 的方式實現(xiàn)了 Selenium 的對接。但這種方法其實是阻塞式的囱修,也就是說這樣就破壞了 Scrapy 異步處理的邏輯赎瑰,速度會受到影響。為了不破壞其異步加載邏輯破镰,我們可以使用 Splash 實現(xiàn)餐曼。下一節(jié)我們再來看看 Scrapy 對接 Splash 的方式。

很多人學習python鲜漩,不知道從何學起源譬。

很多人學習python,掌握了基本語法過后孕似,不知道在哪里尋找案例上手踩娘。

很多已經(jīng)做案例的人,卻不知道如何去學習更加高深的知識喉祭。

那么針對這三類人养渴,我給大家提供一個好的學習平臺,免費領取視頻教程泛烙,電子書籍理卑,以及課程的源代碼!

QQ群:1097524789

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末胶惰,一起剝皮案震驚了整個濱河市傻工,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌孵滞,老刑警劉巖中捆,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異坊饶,居然都是意外死亡泄伪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門匿级,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蟋滴,“玉大人染厅,你說我怎么就攤上這事〗蚝” “怎么了肖粮?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長尔苦。 經(jīng)常有香客問我涩馆,道長,這世上最難降的妖魔是什么允坚? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任魂那,我火速辦了婚禮,結果婚禮上稠项,老公的妹妹穿的比我還像新娘涯雅。我一直安慰自己,他們只是感情好展运,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布活逆。 她就那樣靜靜地躺著,像睡著了一般乐疆。 火紅的嫁衣襯著肌膚如雪划乖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天挤土,我揣著相機與錄音,去河邊找鬼误算。 笑死仰美,一個胖子當著我的面吹牛,可吹牛的內容都是我干的儿礼。 我是一名探鬼主播咖杂,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蚊夫!你這毒婦竟也來了诉字?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤知纷,失蹤者是張志新(化名)和其女友劉穎壤圃,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體琅轧,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡伍绳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了乍桂。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冲杀。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡效床,死狀恐怖,靈堂內的尸體忽然破棺而出权谁,到底是詐尸還是另有隱情剩檀,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布旺芽,位于F島的核電站沪猴,受9級特大地震影響,放射性物質發(fā)生泄漏甥绿。R本人自食惡果不足惜字币,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望共缕。 院中可真熱鬧洗出,春花似錦、人聲如沸图谷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽便贵。三九已至菠镇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間承璃,已是汗流浹背利耍。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盔粹,地道東北人隘梨。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像舷嗡,于是被迫代替她去往敵國和親轴猎。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355