(五)scrapy-redis分布式爬蟲項(xiàng)目實(shí)戰(zhàn)

scrapy-redis分布式組件

由多臺(tái)機(jī)器協(xié)同完成一個(gè)任務(wù),從而縮短任務(wù)的執(zhí)行時(shí)間
優(yōu)點(diǎn):

  • 提升了項(xiàng)目的整體速度
  • 單個(gè)節(jié)點(diǎn)不穩(wěn)定不會(huì)影響整個(gè)任務(wù)執(zhí)行

Scrapy 和 scrapy-redis的區(qū)別

Scrapy 是一個(gè)通用的爬蟲框架,但是不支持分布式,Scrapy-redis是為了更方便地實(shí)現(xiàn)Scrapy分布式爬取,而提供了一些以redis為基礎(chǔ)的組件(僅有組件)。

pip install scrapy-redis

Scrapy-redis提供了下面四種組件(components):(四種組件意味著這四個(gè)模塊都要做相應(yīng)的修改)

  • Scheduler
  • Duplication Filter
  • Item Pipeline
  • Base Spider

scrapy-redis架構(gòu)


如上圖所?示,scrapy-redis在scrapy的架構(gòu)上增加了redis,基于redis的特性拓展了如下組件:

Scheduler:

Scrapy改造了python本來的collection.deque(雙向隊(duì)列)形成了自己的Scrapy queue(https://github.com/scrapy/queuelib/blob/master/queuelib/queue.py)),但是Scrapy多個(gè)spider不能共享待爬取隊(duì)列Scrapy queue, 即Scrapy本身不支持爬蟲分布式,scrapy-redis 的解決是把這個(gè)Scrapy queue換成redis數(shù)據(jù)庫(也是指redis隊(duì)列),從同一個(gè)redis-server存放要爬取的request,便能讓多個(gè)spider去同一個(gè)數(shù)據(jù)庫里讀取。

Scrapy中跟“待爬隊(duì)列”直接相關(guān)的就是調(diào)度器Scheduler,它負(fù)責(zé)對(duì)新的request進(jìn)行入列操作(加入Scrapy queue),取出下一個(gè)要爬取的request(從Scrapy queue中取出)等操作。它把待爬隊(duì)列按照優(yōu)先級(jí)建立了一個(gè)字典結(jié)構(gòu),比如:

 {
        優(yōu)先級(jí)0 : 隊(duì)列0
        優(yōu)先級(jí)1 : 隊(duì)列1
        優(yōu)先級(jí)2 : 隊(duì)列2
    }

然后根據(jù)request中的優(yōu)先級(jí),來決定該入哪個(gè)隊(duì)列,出列時(shí)則按優(yōu)先級(jí)較小的優(yōu)先出列。為了管理這個(gè)比較高級(jí)的隊(duì)列字典,Scheduler需要提供一系列的方法。但是原來的Scheduler已經(jīng)無法使用,所以使用Scrapy-redis的scheduler組件。

Duplication Filter

Scrapy中用集合實(shí)現(xiàn)這個(gè)request去重功能,Scrapy中把已經(jīng)發(fā)送的request指紋放入到一個(gè)集合中,把下一個(gè)request的指紋拿到集合中比對(duì),如果該指紋存在于集合中,說明這個(gè)request發(fā)送過了,如果沒有則繼續(xù)操作。這個(gè)核心的判重功能是這樣實(shí)現(xiàn)的:

def request_seen(self, request):
        # 把請(qǐng)求轉(zhuǎn)化為指紋  
        fp = self.request_fingerprint(request)

        # 這就是判重的核心操作  ,self.fingerprints就是指紋集合
        if fp in self.fingerprints:
            return True  #直接返回
        self.fingerprints.add(fp) #如果不在,就添加進(jìn)去指紋集合
        if self.file:
            self.file.write(fp + os.linesep)

在scrapy-redis中去重是由Duplication Filter組件來實(shí)現(xiàn)的,它通過redis的set 不重復(fù)的特性,巧妙的實(shí)現(xiàn)了Duplication Filter去重。scrapy-redis調(diào)度器從引擎接受request,將request的指紋存?redis的set檢查是否重復(fù),并將不重復(fù)的request push寫?redis的 request queue。

引擎請(qǐng)求request(Spider發(fā)出的)時(shí),調(diào)度器從redis的request queue隊(duì)列?里根據(jù)優(yōu)先級(jí)pop 出?個(gè)request 返回給引擎,引擎將此request發(fā)給spider處理。

Item Pipeline

引擎將(Spider返回的)爬取到的Item給Item Pipeline,scrapy-redis 的Item Pipeline將爬取到的 Item 存?redis的 items queue。

修改過Item Pipeline可以很方便的根據(jù) key 從 items queue 提取item,從?實(shí)現(xiàn) items processes集群。

Base Spider

不在使用scrapy原有的Spider類,重寫的RedisSpider繼承了Spider和RedisMixin這兩個(gè)類,RedisMixin是用來從redis讀取url的類。

當(dāng)我們生成一個(gè)Spider繼承RedisSpider時(shí),調(diào)用setup_redis函數(shù),這個(gè)函數(shù)會(huì)去連接redis數(shù)據(jù)庫,然后會(huì)設(shè)置signals(信號(hào)):

  • 一個(gè)是當(dāng)spider空閑時(shí)候的signal,會(huì)調(diào)用spider_idle函數(shù),這個(gè)函數(shù)調(diào)用schedule_next_request函數(shù),保證spider是一直活著的狀態(tài),并且拋出DontCloseSpider異常。

  • 一個(gè)是當(dāng)抓到一個(gè)item時(shí)的signal,會(huì)調(diào)用item_scraped函數(shù),這個(gè)函數(shù)會(huì)調(diào)用schedule_next_request函數(shù),獲取下一個(gè)request。

Scrapy-Redis分布式策略:

假設(shè)有四臺(tái)電腦:Windows 10、Mac OS X、Ubuntu 16.04、CentOS 7.2,任意一臺(tái)電腦都可以作為 Master端 或 Slaver端,比如:

  • Master端(核心服務(wù)器) :使用 Windows 10,搭建一個(gè)Redis數(shù)據(jù)庫,不負(fù)責(zé)爬取,只負(fù)責(zé)url指紋判重、Request的分配,以及數(shù)據(jù)的存儲(chǔ)

  • Slaver端(爬蟲程序執(zhí)行端) :使用 Mac OS X 、Ubuntu 16.04、CentOS 7.2,負(fù)責(zé)執(zhí)行爬蟲程序,運(yùn)行過程中提交新的Request給Master


  • 首先Slaver端從Master端拿任務(wù)(Request、url)進(jìn)行數(shù)據(jù)抓取,Slaver抓取數(shù)據(jù)的同時(shí),產(chǎn)生新任務(wù)的Request便提交給 Master 處理;

  • Master端只有一個(gè)Redis數(shù)據(jù)庫,負(fù)責(zé)將未處理的Request去重和任務(wù)分配,將處理后的Request加入待爬隊(duì)列,并且存儲(chǔ)爬取的數(shù)據(jù)。
    Scrapy-Redis默認(rèn)使用的就是這種策略,我們實(shí)現(xiàn)起來很簡單,因?yàn)槿蝿?wù)調(diào)度等工作Scrapy-Redis都已經(jīng)幫我們做好了,我們只需要繼承RedisSpider、指定redis_key就行了。

缺點(diǎn)是,Scrapy-Redis調(diào)度的任務(wù)是Request對(duì)象,里面信息量比較大(不僅包含url,還有callback函數(shù)、headers等信息),可能導(dǎo)致的結(jié)果就是會(huì)降低爬蟲速度、而且會(huì)占用Redis大量的存儲(chǔ)空間,所以如果要保證效率,那么就需要一定硬件水平

官方案例

克隆案例到本地

 clone github scrapy-redis源碼文件
git clone https://github.com/rolando/scrapy-redis.git

# 直接拿官方的項(xiàng)目范例,改名為自己的項(xiàng)目用(針對(duì)懶癌患者)
mv scrapy-redis/example-project ~/scrapyredis-project

我們clone到的 scrapy-redis 源碼中有自帶一個(gè)example-project項(xiàng)目,這個(gè)項(xiàng)目包含3個(gè)spider,分別是dmoz, myspider_redis,mycrawler_redis。

一、dmoz (class DmozSpider(CrawlSpider))

這個(gè)爬蟲繼承的是CrawlSpider,它是用來說明Redis的持續(xù)性,當(dāng)我們第一次運(yùn)行dmoz爬蟲,然后Ctrl + C停掉之后,再運(yùn)行dmoz爬蟲,之前的爬取記錄是保留在Redis里的。

分析起來,其實(shí)這就是一個(gè) scrapy-redis 版 CrawlSpider 類,需要設(shè)置Rule規(guī)則,以及callback不能寫parse()方法。
執(zhí)行方式:

scrapy crawl dmoz

代碼如下:


from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule


class DmozSpider(CrawlSpider):
    """Follow categories and extract links."""
    name = 'dmoz'
    allowed_domains = ['dmoztools.net/']
    start_urls = ['http://dmoztools.net/']

    rules = [
        Rule(LinkExtractor(
            restrict_css=('.top-cat', '.sub-cat', '.cat-item')
        ), callback='parse_directory', follow=True),
    ]

    def parse_directory(self, response):
        for div in response.css('.title-and-desc'):
            yield {
                'name': div.css('.site-title::text').extract_first(),
                'description': div.css('.site-descr::text').extract_first().strip(),
                'link': div.css('a::attr(href)').extract_first(),
            }

二、myspider_redis (class MySpider(RedisSpider))

這個(gè)爬蟲繼承了RedisSpider, 它能夠支持分布式的抓取,采用的是basic spider,需要寫parse函數(shù)。

其次就是不再有start_urls了,取而代之的是redis_key,scrapy-redis將key從Redis里pop出來,成為請(qǐng)求的url地址。

from scrapy_redis.spiders import RedisSpider


class MySpider(RedisSpider):
    """Spider that reads urls from redis queue (myspider:start_urls)."""
    name = 'myspider_redis'

    # 注意redis-key的格式:
    redis_key = 'myspider:start_urls'

    # 可選:等效于allowd_domains(),__init__方法按規(guī)定格式寫,使用時(shí)只需要修改super()里的類名參數(shù)即可
    def __init__(self, *args, **kwargs):
        # Dynamically define the allowed domains list.
        domain = kwargs.pop('domain', '')
        self.allowed_domains = filter(None, domain.split(','))

        # 修改這里的類名為當(dāng)前類名
        super(MySpider, self).__init__(*args, **kwargs)

    def parse(self, response):
        return {
            'name': response.css('title::text').extract_first(),
            'url': response.url,
        }

注意:
RedisSpider類 不需要寫allowd_domains和start_urls:

  • scrapy-redis將從在構(gòu)造方法init()里動(dòng)態(tài)定義爬蟲爬取域范圍,也可以選擇直接寫allowd_domains。

  • 必須指定redis_key,即啟動(dòng)爬蟲的命令,參考格式:redis_key = 'myspider:start_urls'

  • 根據(jù)指定的格式,start_urls將在 Master端的 redis-cli 里 lpush 到 Redis數(shù)據(jù)庫里,RedisSpider 將在數(shù)據(jù)庫里獲取start_urls。

執(zhí)行方式:

  1. 通過runspider方法執(zhí)行爬蟲的py文件(也可以分次執(zhí)行多條),爬蟲(們)將處于等待準(zhǔn)備狀態(tài):
scrapy runspider myspider_redis.py
  1. 在Master端的redis-cli輸入push指令,參考格式:
$redis > lpush myspider:start_urls http://dmoztools.net/
  1. Slaver端爬蟲獲取到請(qǐng)求,開始爬取。

三、mycrawler_redis (class MyCrawler(RedisCrawlSpider))

這個(gè)RedisCrawlSpider類爬蟲繼承了RedisCrawlSpider,能夠支持分布式的抓取。因?yàn)椴捎玫氖莄rawlSpider,所以需要遵守Rule規(guī)則,以及callback不能寫parse()方法。

同樣也不再有start_urls了,取而代之的是redis_key,scrapy-redis將key從Redis里pop出來,成為請(qǐng)求的url地址

from scrapy.spiders import Rule
from scrapy.linkextractors import LinkExtractor

from scrapy_redis.spiders import RedisCrawlSpider


class MyCrawler(RedisCrawlSpider):
    """Spider that reads urls from redis queue (myspider:start_urls)."""
    name = 'mycrawler_redis'
    redis_key = 'mycrawler:start_urls'

    rules = (
        # follow all links
        Rule(LinkExtractor(), callback='parse_page', follow=True),
    )

    # __init__方法必須按規(guī)定寫,使用時(shí)只需要修改super()里的類名參數(shù)即可
    def __init__(self, *args, **kwargs):
        # Dynamically define the allowed domains list.
        domain = kwargs.pop('domain', '')
        self.allowed_domains = filter(None, domain.split(','))

        # 修改這里的類名為當(dāng)前類名
        super(MyCrawler, self).__init__(*args, **kwargs)

    def parse_page(self, response):
        return {
            'name': response.css('title::text').extract_first(),
            'url': response.url,
        }

注意:
同樣的,RedisCrawlSpider類不需要寫allowd_domains和start_urls:

  • scrapy-redis將從在構(gòu)造方法init()里動(dòng)態(tài)定義爬蟲爬取域范圍,也可以選擇直接寫allowd_domains。

  • 必須指定redis_key,即啟動(dòng)爬蟲的命令,參考格式:redis_key = 'myspider:start_urls'

  • 根據(jù)指定的格式,start_urls將在 Master端的 redis-cli 里 lpush 到 Redis數(shù)據(jù)庫里,RedisSpider 將在數(shù)據(jù)庫里獲取start_urls。
    執(zhí)行方式:

  1. 通過runspider方法執(zhí)行爬蟲的py文件(也可以分次執(zhí)行多條),爬蟲(們)將處于等待準(zhǔn)備狀態(tài):
scrapy runspider mycrawler_redis.py
  1. 在Master端的redis-cli輸入push指令,參考格式
$redis > lpush mycrawler:start_urls http://www.dmoz.org/
  1. 爬蟲獲取url,開始執(zhí)行。

總結(jié):

  • 如果只是用到Redis的去重和保存功能,就選第一種;

  • 如果要寫分布式,則根據(jù)情況,選擇第二種、第三種;

  • 通常情況下,會(huì)選擇用第三種方式編寫深度聚焦爬蟲。

項(xiàng)目實(shí)戰(zhàn):京東圖書爬蟲

需求:抓取京東圖書信息 目標(biāo)字段: 書名,大分類,大分類頁面url,小分類,小分類頁面url,封面圖片鏈接,詳情頁面url,作者,出版社,出版時(shí)間,價(jià)格 url: https://book.jd.com/booksort.html

分布式爬蟲構(gòu)建的思路:

  • 先完成普通爬蟲
  • 再修改為分布式爬蟲

京東圖書普通爬蟲

新建項(xiàng)目

scrapy startproject JD

然后執(zhí)行

scrapy genspider book jd.com

生成如下目錄結(jié)構(gòu)



修改JD/items.py文件

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy


class JdItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    # 書名,大分類,大分類頁面url,小分類,小分類頁面url,封面圖片鏈接,詳情頁面url,作者,出版社,出版時(shí)間,價(jià)格
    name = scrapy.Field()
    big_category = scrapy.Field()
    big_category_url = scrapy.Field()
    small_category = scrapy.Field()
    small_category_url = scrapy.Field()
    cover_url = scrapy.Field()
    detail_url = scrapy.Field()
    author = scrapy.Field()
    publisher = scrapy.Field()
    pub_date = scrapy.Field()
    price = scrapy.Field()

編寫 JD/spiders/book.py文件

# -*- coding: utf-8 -*-
import scrapy
import json

class BookSpider(scrapy.Spider):
    name = 'book'
    # 'p.3.cn' 為解析圖書列表允許的列名
    allowed_domains = ['jd.com', 'p.3.cn']
    start_urls = ['https://book.jd.com/booksort.html']

    def parse(self, response):
        big_list = response.xpath('//*[@id="booksort"]/div[2]/dl/dt/a')
        print('大節(jié)點(diǎn)',len(big_list))
        for big in big_list:
            # 獲取到大分類的節(jié)點(diǎn)鏈接、節(jié)點(diǎn)名稱
            big_list_url = 'https:' + big.xpath('./@href').extract_first()
            big_category = big.xpath('./text()').extract_first()
            # 小分類的節(jié)點(diǎn)列表
            small_list = big.xpath('../following-sibling::dd[1]/em/a')
            # 遍歷小分類的節(jié)點(diǎn)列表,獲取到小分類名稱、url
            for small in small_list:
                temp = {}
                temp['big_list_url'] = big_list_url
                temp['big_category'] = big_category
                temp['small_category'] = small.xpath('./text()').extract_first()
                temp['small_category_url'] = 'https:' + small.xpath('./@href').extract_first()
                # print(temp)
            # 構(gòu)造請(qǐng)求,返回小分類的url
            yield scrapy.Request(
                temp['small_category_url'],
                callback=self.parse_book_list,
                meta={'meta1': temp}
             )
    # 解析圖片列表信息
    def parse_book_list(self,response):
        # 接受parse方法返回的meta數(shù)據(jù)
        temp = response.meta['meta1']
        # 獲取圖片列表節(jié)點(diǎn)
        book_list = response.xpath('//*[@id="plist"]/ul/li/div')
        # 遍歷圖書列表
        for book in book_list:
            # 實(shí)例化item
            item = JdItem()
            # 書名信息、分類信息
            item['name'] = book.xpath('./div[3]/a/em/text()').extract_first().strip()
            item['big_category'] = temp['big_category']
            item['big_category_url'] = temp['big_list_url']
            item['small_category'] = temp['small_category']
            item['small_category_url'] = temp['small_category_url']
            # /div[1]/a/img/@src
            try:
                item['cover_url'] = 'https:' + book.xpath('./div[1]/a/img/@src').extract_first()
            except:
                item['cover_url'] = None
            try:
                item['detail_url'] = 'https:' + book.xpath('./div[3]/a/@href').extract_first()
            except:
                item['detail_url'] = None
            item['author'] = book.xpath('./div[@class="p-bookdetails"]/span[@class="p-bi-name"]/span[@class="author_type_1"]/a/text()').extract_first()
            item['publisher'] = book.xpath('./div[@class="p-bookdetails"]/span[2]/a/text()').extract_first()
            item['pub_date'] = book.xpath('./div[@class="p-bookdetails"]/span[3]/text()').extract_first().strip()
            # 獲取價(jià)格的url
            # https://p.3.cn/prices/mgets?skuIds=J_11757834%2CJ_10367073%2CJ_11711801%2CJ_12090377%2CJ_10199768%2CJ_11711801%2CJ_12018031%2CJ_10019917%2CJ_11711801%2CJ_10162899%2CJ_11081695%2CJ_12114139%2CJ_12010088%2CJ_12161302%2CJ_11779454%2CJ_11939717%2CJ_12026957%2CJ_12184621%2CJ_12115244%2CJ_11930113%2CJ_10937943%2CJ_12192773%2CJ_12073030%2CJ_12098764%2CJ_11138599%2CJ_11165561%2CJ_11920855%2CJ_11682924%2CJ_11682923%2CJ_11892139&pduid=1523432585886562677791
            skuid = book.xpath('./@data-sku').extract_first()
            # print(skuid)
            pduid = '&pduid=1523432585886562677791'
            print(item)
            # 再次發(fā)送請(qǐng)求,獲取價(jià)格信息
            if skuid is not None:
                url = 'https://p.3.cn/prices/mgets?skuIds=J_' + skuid + pduid
                yield scrapy.Request(
                    url,
                    callback=self.parse_price,
                    meta={'meta2':item}
                )
    # 解析價(jià)格
    def parse_price(self,response):
        item = response.meta['meta2']
        data = json.loads(response.body)
        print(data)
        item['price'] = data[0]['op']
        # print (item)
        yield item

執(zhí)行命令

scrapy crawl book --nolog

可以看到我們成功的爬取了如下的信息


修改成分布式

修改book.py文件

# -*- coding: utf-8 -*-
import scrapy
# 導(dǎo)入item
from JD.items import JdItem
import json


# 改成分布式
# 1------導(dǎo)入類
from scrapy_redis.spiders import RedisSpider

# 2------修改繼承
class BookSpider(RedisSpider):
# class BookSpider(scrapy.Spider):
    name = 'book'
    # 'p.3.cn' 為解析圖書列表允許的列名
    allowed_domains = ['jd.com', 'p.3.cn']
    # start_urls = ['https://book.jd.com/booksort.html']
    # 3------定義redis_key
    redis_key = 'books'
    def parse(self, response):
        big_list = response.xpath('//*[@id="booksort"]/div[2]/dl/dt/a')
        print('大節(jié)點(diǎn)',len(big_list))
        for big in big_list:
            # 獲取到大分類的節(jié)點(diǎn)鏈接、節(jié)點(diǎn)名稱
            big_list_url = 'https:' + big.xpath('./@href').extract_first()
            big_category = big.xpath('./text()').extract_first()
            # 小分類的節(jié)點(diǎn)列表
            small_list = big.xpath('../following-sibling::dd[1]/em/a')
            # 遍歷小分類的節(jié)點(diǎn)列表,獲取到小分類名稱、url
            for small in small_list:
                temp = {}
                temp['big_list_url'] = big_list_url
                temp['big_category'] = big_category
                temp['small_category'] = small.xpath('./text()').extract_first()
                temp['small_category_url'] = 'https:' + small.xpath('./@href').extract_first()
                # print(temp)
            # 構(gòu)造請(qǐng)求,返回小分類的url
            yield scrapy.Request(
                temp['small_category_url'],
                callback=self.parse_book_list,
                meta={'meta1': temp}
             )
    # 解析圖片列表信息
    def parse_book_list(self,response):
        # 接受parse方法返回的meta數(shù)據(jù)
        temp = response.meta['meta1']
        # 獲取圖片列表節(jié)點(diǎn)
        book_list = response.xpath('//*[@id="plist"]/ul/li/div')
        # 遍歷圖書列表
        for book in book_list:
            # 實(shí)例化item
            item = JdItem()
            # 書名信息、分類信息
            item['name'] = book.xpath('./div[3]/a/em/text()').extract_first().strip()
            item['big_category'] = temp['big_category']
            item['big_category_url'] = temp['big_list_url']
            item['small_category'] = temp['small_category']
            item['small_category_url'] = temp['small_category_url']
            # /div[1]/a/img/@src
            try:
                item['cover_url'] = 'https:' + book.xpath('./div[1]/a/img/@src').extract_first()
            except:
                item['cover_url'] = None
            try:
                item['detail_url'] = 'https:' + book.xpath('./div[3]/a/@href').extract_first()
            except:
                item['detail_url'] = None
            item['author'] = book.xpath('./div[@class="p-bookdetails"]/span[@class="p-bi-name"]/span[@class="author_type_1"]/a/text()').extract_first()
            item['publisher'] = book.xpath('./div[@class="p-bookdetails"]/span[2]/a/text()').extract_first()
            item['pub_date'] = book.xpath('./div[@class="p-bookdetails"]/span[3]/text()').extract_first().strip()
            # 獲取價(jià)格的url
            # https://p.3.cn/prices/mgets?skuIds=J_11757834%2CJ_10367073%2CJ_11711801%2CJ_12090377%2CJ_10199768%2CJ_11711801%2CJ_12018031%2CJ_10019917%2CJ_11711801%2CJ_10162899%2CJ_11081695%2CJ_12114139%2CJ_12010088%2CJ_12161302%2CJ_11779454%2CJ_11939717%2CJ_12026957%2CJ_12184621%2CJ_12115244%2CJ_11930113%2CJ_10937943%2CJ_12192773%2CJ_12073030%2CJ_12098764%2CJ_11138599%2CJ_11165561%2CJ_11920855%2CJ_11682924%2CJ_11682923%2CJ_11892139&pduid=1523432585886562677791
            skuid = book.xpath('./@data-sku').extract_first()
            # print(skuid)
            pduid = '&pduid=1523432585886562677791'
            print(item)
            # 再次發(fā)送請(qǐng)求,獲取價(jià)格信息
            if skuid is not None:
                url = 'https://p.3.cn/prices/mgets?skuIds=J_' + skuid + pduid
                yield scrapy.Request(
                    url,
                    callback=self.parse_price,
                    meta={'meta2':item}
                )
    # 解析價(jià)格
    def parse_price(self,response):
        item = response.meta['meta2']
        data = json.loads(response.body)
        print(data)
        item['price'] = data[0]['op']
        # print (item)
        yield item

注釋原來的JD/settings.py文件,更改如下

# -*- coding: utf-8 -*-
import scrapy
# 導(dǎo)入item
from JD.items import JdItem
import json


# 改成分布式
# 1------導(dǎo)入類
from scrapy_redis.spiders import RedisSpider

# 2------修改繼承
class BookSpider(RedisSpider):
# class BookSpider(scrapy.Spider):
    name = 'book'
    # 'p.3.cn' 為解析圖書列表允許的列名
    allowed_domains = ['jd.com', 'p.3.cn']
    # start_urls = ['https://book.jd.com/booksort.html']
    # 3------定義redis_key
    redis_key = 'books'
    def parse(self, response):
        big_list = response.xpath('//*[@id="booksort"]/div[2]/dl/dt/a')
        print('大節(jié)點(diǎn)',len(big_list))
        for big in big_list:
            # 獲取到大分類的節(jié)點(diǎn)鏈接、節(jié)點(diǎn)名稱
            big_list_url = 'https:' + big.xpath('./@href').extract_first()
            big_category = big.xpath('./text()').extract_first()
            # 小分類的節(jié)點(diǎn)列表
            small_list = big.xpath('../following-sibling::dd[1]/em/a')
            # 遍歷小分類的節(jié)點(diǎn)列表,獲取到小分類名稱、url
            for small in small_list:
                temp = {}
                temp['big_list_url'] = big_list_url
                temp['big_category'] = big_category
                temp['small_category'] = small.xpath('./text()').extract_first()
                temp['small_category_url'] = 'https:' + small.xpath('./@href').extract_first()
                # print(temp)
            # 構(gòu)造請(qǐng)求,返回小分類的url
            yield scrapy.Request(
                temp['small_category_url'],
                callback=self.parse_book_list,
                meta={'meta1': temp}
             )
    # 解析圖片列表信息
    def parse_book_list(self,response):
        # 接受parse方法返回的meta數(shù)據(jù)
        temp = response.meta['meta1']
        # 獲取圖片列表節(jié)點(diǎn)
        book_list = response.xpath('//*[@id="plist"]/ul/li/div')
        # 遍歷圖書列表
        for book in book_list:
            # 實(shí)例化item
            item = JdItem()
            # 書名信息、分類信息
            item['name'] = book.xpath('./div[3]/a/em/text()').extract_first().strip()
            item['big_category'] = temp['big_category']
            item['big_category_url'] = temp['big_list_url']
            item['small_category'] = temp['small_category']
            item['small_category_url'] = temp['small_category_url']
            # /div[1]/a/img/@src
            try:
                item['cover_url'] = 'https:' + book.xpath('./div[1]/a/img/@src').extract_first()
            except:
                item['cover_url'] = None
            try:
                item['detail_url'] = 'https:' + book.xpath('./div[3]/a/@href').extract_first()
            except:
                item['detail_url'] = None
            item['author'] = book.xpath('./div[@class="p-bookdetails"]/span[@class="p-bi-name"]/span[@class="author_type_1"]/a/text()').extract_first()
            item['publisher'] = book.xpath('./div[@class="p-bookdetails"]/span[2]/a/text()').extract_first()
            item['pub_date'] = book.xpath('./div[@class="p-bookdetails"]/span[3]/text()').extract_first().strip()
            # 獲取價(jià)格的url
            # https://p.3.cn/prices/mgets?skuIds=J_11757834%2CJ_10367073%2CJ_11711801%2CJ_12090377%2CJ_10199768%2CJ_11711801%2CJ_12018031%2CJ_10019917%2CJ_11711801%2CJ_10162899%2CJ_11081695%2CJ_12114139%2CJ_12010088%2CJ_12161302%2CJ_11779454%2CJ_11939717%2CJ_12026957%2CJ_12184621%2CJ_12115244%2CJ_11930113%2CJ_10937943%2CJ_12192773%2CJ_12073030%2CJ_12098764%2CJ_11138599%2CJ_11165561%2CJ_11920855%2CJ_11682924%2CJ_11682923%2CJ_11892139&pduid=1523432585886562677791
            skuid = book.xpath('./@data-sku').extract_first()
            # print(skuid)
            pduid = '&pduid=1523432585886562677791'
            print(item)
            # 再次發(fā)送請(qǐng)求,獲取價(jià)格信息
            if skuid is not None:
                url = 'https://p.3.cn/prices/mgets?skuIds=J_' + skuid + pduid
                yield scrapy.Request(
                    url,
                    callback=self.parse_price,
                    meta={'meta2':item}
                )
    # 解析價(jià)格
    def parse_price(self,response):
        item = response.meta['meta2']
        data = json.loads(response.body)
        print(data)
        item['price'] = data[0]['op']
        # print (item)
        yield item

完成以上代買后我們開啟兩個(gè)終端:
執(zhí)行命令:

 scrapy runspider book.py

在redis安裝目錄啟動(dòng):

redis-cli.exe

我們可以看到分布式爬蟲運(yùn)行起來了



使用 Redis Desktop Manager查看數(shù)據(jù)


數(shù)據(jù)持久化 保存至MongoDB中

  • 什么是數(shù)據(jù)持久化 : 所謂數(shù)據(jù)持久化就是將redis中存儲(chǔ)的item數(shù)據(jù)存儲(chǔ)到其他數(shù)據(jù)庫或介質(zhì)中
  • 為什么要做數(shù)據(jù)持久化處理 1)redis是內(nèi)存型數(shù)據(jù)庫,容量有限 2)內(nèi)存在斷電時(shí)會(huì)丟失所有數(shù)據(jù),不安全 3)數(shù)據(jù)的使用一般不使用redis

如何將數(shù)據(jù)持久化

  • 將redis數(shù)據(jù)庫中的數(shù)據(jù)讀出,存放到其他類型的數(shù)據(jù)庫中
  • Python redis庫 1.鏈接: redis.Redis(host,port,db) 2.讀取: 以先進(jìn)先出的形式讀取數(shù)據(jù) source,data = redis.blpop(keylist) 以先進(jìn)后出的形式讀取數(shù)據(jù) source,data = redis.brpop(keylist)

將爬取的京東圖書從redis中取出然后保存至MongoDB中

開啟mongodb數(shù)據(jù)庫



新建redis_mongo.py文件,執(zhí)行如下代碼

# 數(shù)據(jù)的持久化操作redis---->MongoDB
import redis
from pymongo import MongoClient
import json

# 實(shí)例化redis客戶端
redis_client = redis.Redis(host='127.0.0.1',port=6379)

# 實(shí)例化MongoDB客戶端
mongo_client = MongoClient(host='127.0.0.1',port=27017)

# 指定鏈接的MongDB數(shù)據(jù)庫、集合
db = mongo_client['CRAWL']
col = db['crawl']
# 使用循環(huán)把redis中數(shù)據(jù)全部寫入到MongoDB中
while True:
    # 從redis中取出數(shù)據(jù)
    key,data = redis_client.blpop(['book:items'])
    print(key)
    print(data)

    # 把數(shù)據(jù)寫入到MongoDB中
    col.insert(json.loads(data.decode()))


# 關(guān)閉數(shù)據(jù)庫
mongo_client.close()


使用robo mongo查看數(shù)據(jù)是否寫入數(shù)據(jù)庫中


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,125評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,402評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,934評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,168評(píng)論 0 287
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,690評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,596評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評(píng)論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,027評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評(píng)論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,398評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,743評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容