(五)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ù)庫(kù)(也是指redis隊(duì)列)息裸,從同一個(gè)redis-server存放要爬取的request,便能讓多個(gè)spider去同一個(gè)數(shù)據(jù)庫(kù)里讀取沪编。

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ù)庫(kù)捎琐,然后會(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ù)庫(kù)哪工,不負(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ù)庫(kù),負(fù)責(zé)將未處理的Request去重和任務(wù)分配祭钉,將處理后的Request加入待爬隊(duì)列瞄沙,并且存儲(chǔ)爬取的數(shù)據(jù)距境。
    Scrapy-Redis默認(rèn)使用的就是這種策略遂铡,我們實(shí)現(xiàn)起來很簡(jiǎ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ù)庫(kù)里,RedisSpider 將在數(shù)據(jù)庫(kù)里獲取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ù)庫(kù)里氮墨,RedisSpider 將在數(shù)據(jù)庫(kù)里獲取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)字段: 書名萤捆,大分類俗或,大分類頁(yè)面url辛慰,小分類帅腌,小分類頁(yè)面url速客,封面圖片鏈接溺职,詳情頁(yè)面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()
    # 書名颠区,大分類毕莱,大分類頁(yè)面url朋截,小分類部服,小分類頁(yè)面url廓八,封面圖片鏈接剧蹂,詳情頁(yè)面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ù)庫(kù)或介質(zhì)中
  • 為什么要做數(shù)據(jù)持久化處理 1)redis是內(nèi)存型數(shù)據(jù)庫(kù),容量有限 2)內(nèi)存在斷電時(shí)會(huì)丟失所有數(shù)據(jù)啊掏,不安全 3)數(shù)據(jù)的使用一般不使用redis

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

  • 將redis數(shù)據(jù)庫(kù)中的數(shù)據(jù)讀出,存放到其他類型的數(shù)據(jù)庫(kù)中
  • Python redis庫(kù) 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ù)庫(kù)



新建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ù)庫(kù)卦睹、集合
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ù)庫(kù)
mongo_client.close()


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


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末纵潦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子返敬,更是在濱河造成了極大的恐慌劲赠,老刑警劉巖凛澎,帶你破解...
    沈念sama閱讀 217,084評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件塑煎,死亡現(xiàn)場(chǎng)離奇詭異轧叽,居然都是意外死亡炭晒,警方通過查閱死者的電腦和手機(jī)网严,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門震束,熙熙樓的掌柜王于貴愁眉苦臉地迎上來垢村,“玉大人,你說我怎么就攤上這事宏榕÷橹纾” “怎么了抚芦?”我有些...
    開封第一講書人閱讀 163,450評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵叉抡,是天一觀的道長(zhǎng)卜壕。 經(jīng)常有香客問我轴捎,道長(zhǎng)侦副,這世上最難降的妖魔是什么驼鞭? 我笑而不...
    開封第一講書人閱讀 58,322評(píng)論 1 293
  • 正文 為了忘掉前任挣棕,我火速辦了婚禮译隘,結(jié)果婚禮上亲桥,老公的妹妹穿的比我還像新娘。我一直安慰自己固耘,他們只是感情好题篷,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評(píng)論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著厅目,像睡著了一般番枚。 火紅的嫁衣襯著肌膚如雪损敷。 梳的紋絲不亂的頭發(fā)上葫笼,一...
    開封第一講書人閱讀 51,274評(píng)論 1 300
  • 那天,我揣著相機(jī)與錄音拗馒,去河邊找鬼路星。 笑死,一個(gè)胖子當(dāng)著我的面吹牛诱桂,可吹牛的內(nèi)容都是我干的奥额。 我是一名探鬼主播,決...
    沈念sama閱讀 40,126評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼访诱,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了韩肝?” 一聲冷哼從身側(cè)響起触菜,我...
    開封第一講書人閱讀 38,980評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哀峻,沒想到半個(gè)月后涡相,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,414評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡剩蟀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評(píng)論 3 334
  • 正文 我和宋清朗相戀三年催蝗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片育特。...
    茶點(diǎn)故事閱讀 39,773評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡丙号,死狀恐怖丈莺,靈堂內(nèi)的尸體忽然破棺而出秕脓,到底是詐尸還是另有隱情,我是刑警寧澤恒削,帶...
    沈念sama閱讀 35,470評(píng)論 5 344
  • 正文 年R本政府宣布棉浸,位于F島的核電站怀薛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏迷郑。R本人自食惡果不足惜枝恋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評(píng)論 3 327
  • 文/蒙蒙 一创倔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧焚碌,春花似錦畦攘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至摆出,卻和暖如春朗徊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背偎漫。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工爷恳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人象踊。 一個(gè)月前我還...
    沈念sama閱讀 47,865評(píng)論 2 370
  • 正文 我出身青樓温亲,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親杯矩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子栈虚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評(píng)論 2 354

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