1. 程式人生 > >Scrapy框架之基於RedisSpider實現的分散式爬蟲

Scrapy框架之基於RedisSpider實現的分散式爬蟲

需求:爬取的是基於文字的網易新聞資料(國內、國際、軍事、航空)。
  基於Scrapy框架程式碼實現資料爬取後,再將當前專案修改為基於RedisSpider的分散式爬蟲形式。

一、基於Scrapy框架資料爬取實現

1、專案和爬蟲檔案建立

$ scrapy startproject wangyiPro
$ cd wangyiPro/
$ scrapy genspider wangyi news.163.com    # 基於scrapy.Spider建立爬蟲檔案

2、爬蟲檔案編寫——解析新聞首頁獲取四個板塊的url

import scrapy

class WangyiSpider(scrapy.Spider):
    name = 'wangyi'
    # allowed_domains = ['news.163.com']
    start_urls = ['https://news.163.com/']

    def parse(self, response):
        lis = response.xpath('//div[@class="ns_area list"]/ul/li')
        # 獲取指定的四個列表元素(國內3、國際5、軍事6、航空7)
        indexes = [3, 4, 6, 7]
        li_list = []   # 四個板塊對應的li標籤物件
        for index in indexes:
            li_list.append(lis[index])

        # 獲取四個板塊中的超鏈和文字標題
        for li in li_list:
            url = li.xpath('./a/@href').extract_first()
            title = li.xpath('./a/text()').extract_first()   # 板塊名稱

            print(url + ":" + title)   # 測試

  執行爬蟲檔案,控制檯列印輸出四個url,說明解析成功:

$ scrapy crawl wangyi --nolog
http://news.163.com/domestic/:國內
http://news.163.com/world/:國際
http://war.163.com/:軍事
http://news.163.com/air/:航空

3、爬蟲檔案編寫——對每個板塊url發請求,進一步解析

import scrapy

class WangyiSpider(scrapy.Spider):
    name = 'wangyi'
    # allowed_domains = ['news.163.com']
    start_urls = ['https://news.163.com/']

    def parse(self, response):
        lis = response.xpath('//div[@class="ns_area list"]/ul/li')
        # 獲取指定的四個列表元素(國內3、國際5、軍事6、航空7)
        indexes = [3, 4, 6, 7]
        li_list = []   # 四個板塊對應的li標籤物件
        for index in indexes:
            li_list.append(lis[index])

        # 獲取四個板塊中的超鏈和文字標題
        for li in li_list:
            url = li.xpath('./a/@href').extract_first()
            title = li.xpath('./a/text()').extract_first()   # 板塊名稱

            """對每一個板塊對應url發起請求,獲取頁面資料"""
            # 呼叫scrapy.Request()方法發起get請求
            yield scrapy.Request(url=url, callback=self.parseSecond)

    def parseSecond(self, response):
        """宣告回撥函式"""
        # 找到頁面中新聞的共有標籤型別,排除廣告標籤
        div_list = response.xpath('//div[@class="data_row news_article clearfix"]')
        print(len(div_list))   # 非空則驗證xpath是正確的
        for div in div_list:
            # 文章標題
            head = div.xpath('.//div[@class="news_title"]/h3/a/text()').extract_first()
            # 文章url
            url = div.xpath('.//div[@class="news_title"]/h3/a/@href').extract_first()
            # 縮圖
            imgUrl = div.xpath('./a/img/@src').extract_first()
            # 釋出時間和標籤:提取列表中所有的元素
            tag = div.xpath('.//div[@class="news_tag"]//text()').extract()
            # 列表裝化為字串
            tag = "".join(tag)

  編寫到這裡時,再次執行爬蟲指令碼,會發現print(len(div_list))輸出的是4個0,但是xpath表示式卻是正確的。
  這是由於新浪網的新聞列表資訊是動態載入的,而爬蟲程式向url發請求無法獲取動態載入的頁面資訊。
  因此需要selenium幫忙在程式中例項化一個瀏覽器物件,由瀏覽器物件向url發請求,再通過呼叫page_source屬性拿到selenium例項化物件中獲取的頁面資料,這個資料中包含動態載入的資料內容。

二、將selenium應用到Scrapy專案中

  需求分析:當點選國內超鏈進入國內對應的頁面時,會發現當前頁面展示的新聞資料是被動態加載出來的,如果直接通過程式對url進行請求,是獲取不到動態加載出的新聞資料的。則就需要我們使用selenium例項化一個瀏覽器物件,在該物件中進行url的請求,獲取動態載入的新聞資料。
  響應物件response從下載器傳給Spiders爬蟲檔案時,一定會穿過下載中介軟體。
  可以在下載中介軟體對響應物件進行攔截,對響應物件中儲存的頁面資料進行篡改,將動態載入的頁面資料加入到響應物件中。
  通過selenium可以篡改響應資料,並將頁面資料篡改成攜帶了新聞資料的資料。

1、selenium在scrapy中使用原理

  當引擎將國內板塊url對應的請求提交給下載器後,下載器進行網頁資料的下載,然後將下載到的頁面資料,封裝到response中,提交給引擎,引擎將response在轉交給Spiders。
  Spiders接受到的response物件中儲存的頁面資料裡是沒有動態載入的新聞資料的。要想獲取動態載入的新聞資料,則需要在下載中介軟體中對下載器提交給引擎的response響應物件進行攔截,切對其內部儲存的頁面資料進行篡改,修改成攜帶了動態加載出的新聞資料,然後將被篡改的response物件最終交給Spiders進行解析操作。

2、selenium在scrapy中使用流程總結

(1)在爬蟲檔案中匯入webdriver類

from selenium import webdriver

(2)重寫爬蟲檔案的構造方法
  在構造方法中使用selenium例項化一個瀏覽器物件(因為瀏覽器物件只需要被例項化一次)

class WangyiSpider(scrapy.Spider):
    def __init__(self):
        # 例項化瀏覽器物件(保證只會被例項化一次)
        self.bro = webdriver.Chrome(executable_path='/Users/hqs/ScrapyProjects/wangyiPro/wangyiPro/chromedriver')

(3)重寫爬蟲檔案的closed(self,spider)方法
  在其內部關閉瀏覽器物件。該方法是在爬蟲結束時被呼叫。

class WangyiSpider(scrapy.Spider):
    def closed(self, spider):
        # 必須在整個爬蟲結束後關閉瀏覽器
        print('爬蟲結束')
        self.bro.quit()   # 瀏覽器關閉

(4)重寫下載中介軟體的process_response方法
  讓process_response方法對響應物件進行攔截,並篡改response中儲存的頁面資料。

(5)在配置檔案中開啟下載中介軟體

3、專案程式碼示例

(1)引入selenium定義瀏覽器開啟和關閉

import scrapy
from selenium import webdriver
from wangyiPro.items import WangyiproItem

class WangyiSpider(scrapy.Spider):
    name = 'wangyi'
    # allowed_domains = ['news.163.com']
    start_urls = ['https://news.163.com/']

    def __init__(self):
        # 例項化瀏覽器物件(保證只會被例項化一次)
        self.bro = webdriver.Chrome(executable_path='./wangyiPro/chromedrive')


    def closed(self, spider):
        # 必須在整個爬蟲結束後關閉瀏覽器
        print('爬蟲結束')
        self.bro.quit()   # 瀏覽器關閉

(2)使用下載中介軟體攔截settings.py修改

# Enable or disable downloader middlewares
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
    'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
}

(3)在下載中介軟體中進行攔截

  讓瀏覽器物件去發起get請求,獲取四大版塊對應的頁面資料,瀏覽器對url傳送請求,瀏覽器是可以獲取到動態載入的頁面資料的。
  獲取到這部分動態資料後,可以將這部分資料裝回到攔截的response物件中去。然後將篡改好的response物件發給Spiders。
  Spiders接收到response物件後,將response賦值給回撥函式parseSecond的response引數中。
  middlewares.py內容如下所示:

# 下載中介軟體
from scrapy.http import HtmlResponse   # 通過這個類例項化的物件就是響應物件
import time

class WangyiproDownloaderMiddleware(object):
    def process_request(self, request, spider):
        """
        可以攔截請求
        :param request:
        :param spider:
        :return:
        """
        return None

    def process_response(self, request, response, spider):
        """
        可以攔截響應物件(下載器傳遞給Spider的響應物件)
        :param request: 響應物件對應的請求物件
        :param response: 攔截到的響應物件
        :param spider: 爬蟲檔案中對應的爬蟲類的例項
        :return:
        """
        print(request.url + "這是下載中介軟體")
        # 響應物件中儲存頁面資料的篡改
        if request.url in ['http://news.163.com/domestic/', 'http://news.163.com/world/', 'http://war.163.com/', 'http://news.163.com/air/']:
            # 瀏覽器請求傳送(排除起始url)
            spider.bro.get(url=request.url)
            # 滾輪拖動到底部會動態載入新聞資料,js操作滾輪拖動
            js = 'window.scrollTo(0, document.body.scrollHeight)'  # 水平方向不移動:0;豎直方向移動:視窗高度
            spider.bro.execute_script(js)  # 拖動到底部,獲取更多頁面資料
            time.sleep(2)  # js執行給頁面2秒時間緩衝,讓所有資料得以載入
            # 頁面資料page_text包含了動態加載出來的新聞資料對應的頁面資料
            page_text = spider.bro.page_source
            # current_url就是通過瀏覽器發起請求所對應的url
            # body是當前響應物件攜帶的資料值
            return HtmlResponse(url=spider.bro.current_url, body=page_text, encoding="utf-8", request=request)
        else:
            # 四個板塊之外的響應物件不做修改
            return response   # 這是原來的響應物件

三、爬蟲程式碼完善及item處理

1、爬蟲檔案

import scrapy
from selenium import webdriver

from wangyiPro.items import WangyiproItem


class WangyiSpider(scrapy.Spider):
    name = 'wangyi'
    # allowed_domains = ['news.163.com']
    start_urls = ['https://news.163.com/']

    def __init__(self):
        # 例項化瀏覽器物件(保證只會被例項化一次)
        self.bro = webdriver.Chrome(executable_path='/Users/hqs/ScrapyProjects/wangyiPro/wangyiPro/chromedriver')

    def closed(self, spider):
        # 必須在整個爬蟲結束後關閉瀏覽器
        print('爬蟲結束')
        self.bro.quit()   # 瀏覽器關閉

    def parse(self, response):
        lis = response.xpath('//div[@class="ns_area list"]/ul/li')
        # 獲取指定的四個列表元素(國內3、國際5、軍事6、航空7)
        indexes = [3, 4, 6, 7]
        li_list = []   # 四個板塊對應的li標籤物件
        for index in indexes:
            li_list.append(lis[index])

        # 獲取四個板塊中的超鏈和文字標題
        for li in li_list:
            url = li.xpath('./a/@href').extract_first()
            title = li.xpath('./a/text()').extract_first()   # 板塊名稱

            """對每一個板塊對應url發起請求,獲取頁面資料"""
            # 呼叫scrapy.Request()方法發起get請求
            yield scrapy.Request(url=url, callback=self.parseSecond, meta={'title': title})

    def parseSecond(self, response):
        """宣告回撥函式"""
        # 找到頁面中新聞的共有標籤型別,排除廣告標籤
        div_list = response.xpath('//div[@class="data_row news_article clearfix"]')
        # print(len(div_list))   # 非空則驗證xpath是正確的
        for div in div_list:
            # 文章標題
            head = div.xpath('.//div[@class="news_title"]/h3/a/text()').extract_first()
            # 文章url
            url = div.xpath('.//div[@class="news_title"]/h3/a/@href').extract_first()
            # 縮圖
            imgUrl = div.xpath('./a/img/@src').extract_first()
            # 釋出時間和標籤:提取列表中所有的元素
            tag = div.xpath('.//div[@class="news_tag"]//text()').extract()

            # 列表裝化為字串
            tags = []
            for t in tag:
                t = t.strip(' \n \t')   # 去除空格 \n換行 \t相當於tab
                tags.append(t)   # 重新裝載到列表中
            tag = "".join(tags)

            # 獲取meta傳遞的資料值
            title = response.meta['title']

            # 例項化item物件,將解析到的資料值儲存到item物件中
            item = WangyiproItem()
            item['head'] = head
            item['url'] = url
            item['imgUrl'] = imgUrl
            item['tag'] = tag
            item['title'] = title

            # 對url發起請求,獲取對應頁面中儲存的新聞內容資料
            yield scrapy.Request(url=url, callback=self.getContent, meta={"item":item})

    def getContent(self, response):
        """新聞內容解析的回撥函式"""
        # 獲取傳遞過來的item物件
        item = response.meta['item']

        # 解析當前頁碼中儲存的頁面資料
        # 由於新聞的段落可能有多個,每個段落在一個p標籤中。因此使用extract()方法
        content_list = response.xpath('//div[@class="post_text"]/p/text()').extract()

        # 列表轉字串(字串才能保持在item物件中)
        content = "".join(content_list)
        item["content"] = content

        # item物件提交給管道
        yield item

注意:

(1)將解析到的資料值儲存到item物件

  由於爬蟲做了兩次解析,因此如何將第一次解析的資料加入item物件是最大的難點。
  解決方法:meta屬性請求傳參。

# 對url發起請求,獲取對應頁面中儲存的新聞內容資料
yield scrapy.Request(url=url, callback=self.getContent, meta={"item":item})

  對文章url發起請求,欲獲取對應頁面中儲存的新聞內容資料,呼叫新的回撥函式getContent。

(2)新聞內容解析後將item物件提交給管道

class WangyiSpider(scrapy.Spider):
    """同上省略"""
    def getContent(self, response):
        """新聞內容解析的回撥函式"""
        # 獲取傳遞過來的item物件
        item = response.meta['item']

        # 解析當前頁碼中儲存的頁面資料
        # 由於新聞的段落可能有多個,每個段落在一個p標籤中。因此使用extract()方法
        content_list = response.xpath('//div[@class="post_text"]/p/text()').extract()

        # 列表轉字串(字串才能保持在item物件中)
        content = "".join(content_list)
        item["content"] = content

        # item物件提交給管道
        yield item

2、items.py檔案

import scrapy

class WangyiproItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    head = scrapy.Field()
    url = scrapy.Field()
    imgUrl = scrapy.Field()
    tag = scrapy.Field()
    title = scrapy.Field()
    content = scrapy.Field()

3、管道檔案pipeline.py處理

(1)pipelines.py

class WangyiproPipeline(object):
    def process_item(self, item, spider):
        print(item['title']+ ':'+ item['content'])
        return item

(2)settings.py中放開管道

# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
    'wangyiPro.pipelines.WangyiproPipeline': 300,
}

(3)執行爬蟲輸出爬取的新聞資訊

爬取結果
  

四、UA池和代理池在Scrapy中應用