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í)行方式:
- 通過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ù)庫(kù)里氮墨,RedisSpider 將在數(shù)據(jù)庫(kù)里獲取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)字段: 書名萤捆,大分類俗或,大分類頁(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ù)中