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í)行方式:
- 通過runspider方法執(zhí)行爬蟲的py文件(也可以分次執(zhí)行多條),爬蟲(們)將處于等待準(zhǔn)備狀態(tài):
scrapy runspider myspider_redis.py
- 在Master端的redis-cli輸入push指令,參考格式:
$redis > lpush myspider:start_urls http://dmoztools.net/
- 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í)行方式:
- 通過runspider方法執(zhí)行爬蟲的py文件(也可以分次執(zhí)行多條),爬蟲(們)將處于等待準(zhǔn)備狀態(tài):
scrapy runspider mycrawler_redis.py
- 在Master端的redis-cli輸入push指令,參考格式
$redis > lpush mycrawler:start_urls http://www.dmoz.org/
- 爬蟲獲取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ù)庫中