爬虫:CrawlSpider + 分布式爬虫 + 增量式爬虫
CrawlSpider(爬取多页面数据)
scrapy startproject baike #创建 baike 的项目 cd baike scrapy genspider -t crawl duanzi www.xxx.com #创建爬虫文件 duanzi scrapy crawl suanzi #执行程序,要找对duanzi.py的路径 这里这个执行跟其他的执行有点不同
CrawlSpider的介绍
引入
提问:如果想要通过爬虫程序去爬取”糗百“全站数据新闻数据的话,有几种实现方法?
方法一:基于Scrapy框架中的Spider的递归爬取进行实现(Request模块递归回调parse方法)。
方法二:基于CrawlSpider的自动爬取进行实现(更加简洁和高效)。
今日概要
- CrawlSpider简介
- CrawlSpider使用
- 基于CrawlSpider爬虫文件的创建
- 链接提取器
- 规则解析器
今日详情
一.简介
CrawlSpider其实是Spider的一个子类,除了继承到Spider的特性和功能外,还派生除了其自己独有的更加强大的特性和功能。其中最显著的功能就是”LinkExtractors链接提取器“。Spider是所有爬虫的基类,其设计原则只是为了爬取start_url列表中网页,而从爬取到的网页中提取出的url进行继续的爬取工作使用CrawlSpider更合适。
二.使用
1.创建scrapy工程:scrapy startproject projectName
2.创建爬虫文件:scrapy genspider -t crawl spiderName www.xxx.com
--此指令对比以前的指令多了 "-t crawl",表示创建的爬虫文件是基于CrawlSpider这个类的,而不再是Spider这个基类。
3.观察生成的爬虫文件
- 2,3行:导入CrawlSpider相关模块
- 7行:表示该爬虫程序是基于CrawlSpider类的
- 12,13,14行:表示为提取Link规则
- 16行:解析方法
CrawlSpider类和Spider类的最大不同是CrawlSpider多了一个rules属性,其作用是定义”提取动作“。在rules中可以包含一个或多个Rule对象,在Rule对象中包含了LinkExtractor对象。
3.1 LinkExtractor:顾名思义,链接提取器。
LinkExtractor(
allow=r'Items/',# 满足括号中“正则表达式”的值会被提取,如果为空,则全部匹配。
deny=xxx, # 满足正则表达式的则不会被提取。
restrict_xpaths=xxx, # 满足xpath表达式的值会被提取
restrict_css=xxx, # 满足css表达式的值会被提取
deny_domains=xxx, # 不会被提取的链接的domains。
)
- 作用:提取response中符合规则的链接。
3.2 Rule : 规则解析器。根据链接提取器中提取到的链接,根据指定规则提取解析器链接网页中的内容。
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True)
- 参数介绍:
参数1:指定链接提取器
参数2:指定规则解析器解析数据的规则(回调函数)
参数3:是否将链接提取器继续作用到链接提取器提取出的链接网页中。当callback为None,参数3的默认值为true。
3.3 rules=( ):指定不同规则解析器。一个Rule对象表示一种提取规则。
3.4 CrawlSpider整体爬取流程:
a)爬虫文件首先根据起始url,获取该url的网页内容
b)链接提取器会根据指定提取规则将步骤a中网页内容中的链接进行提取
c)规则解析器会根据指定解析规则将链接提取器中提取到的链接中的网页内容根据指定的规则进行解析
d)将解析数据封装到item中,然后提交给管道进行持久化存储
需求:爬取趣事百科中所有的段子(包含1-35页)
# qiubai.py
import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class ChoutiSpider(CrawlSpider): name = 'qiubai' # 执行的时候根据这个name名来执行该文件,不是该文件的名称,不过默认生成的该名称和文件名一致 # allowed_domains = ['www.xxx.com'] start_urls = ['https://www.qiushibaike.com/pic/'] # 起始url # rules规则解析器:将链接提取器提取到的连接所对应的页面数据进行指定形式的解析,正则表达式 # follow =True ,让连接提取器继续作用到链接提取器提取到的连接所对应的页面中,False只获取起始url页面中连接 rules = ( Rule(LinkExtractor(allow=r'/pic/page/\d+\?s=\d+'), callback='parse_item', follow=True), #正则匹配的是第几页按钮中的,a标签中的href所对应的连接 Rule(LinkExtractor(allow=r"/pic/$"),callback='parse_item',follow=True) ) def parse_item(self, response): print('==>>>>',response) #可以打印出解析出来的页面的连接 # response.xapth() #这里直接用来解析想要的数据即可
# settings.py
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36' ROBOTSTXT_OBEY = False
总结:
1、该crawlSpider适用于解析多页面的网页,比如含有第一页,第二页... 等等,页面数据一样,采用的解析方法也是一样的,
2、当页码中所对应的连接url,不一致时,可以写两条Rule规则来匹配
3、follow=True,让连接提取器继续作用到链接提取器提取到的连接所对应的页面中
基于scrapy-redis分布式爬虫
文件的创建和执行的命令:
scrapy startproject redis_chouti #创建分布式项目redis_chouti scrapy genspider -t crawl chouti www.xxx.com #创建爬虫文件chouti scrapy runspider ./redis_chouti/spiders/chouti.py #执行程序,文件名,执行之后会夯住,等待给定的起始url
#redis-cli客户端中执行:给一个起始的url lpush chouti http://dig.chouti.com #chouti指的是redis_key调度器,不是爬虫的文件名称
一、redis分布式部署
- 为什么原生的scrapy不能实现分布式? - 调度器不能被共享 - 管道无法被共享 - scrapy-redis组件的作用是什么? - 提供了可以被共享的调度器和管道 - 分布式爬虫实现流程 1.环境安装:pip install scrapy-redis 2.创建工程 3.创建爬虫文件:RedisCrawlSpider RedisSpider - scrapy genspider -t crawl xxx www.xxx.com 4.对爬虫文件中的相关属性进行修改: - 导报:from scrapy_redis.spiders import RedisCrawlSpider - 将当前爬虫文件的父类设置成RedisCrawlSpider - 将起始url列表替换成redis_key = 'xxx'(调度器队列的名称) 5.在配置文件中进行配置: - 使用组件中封装好的可以被共享的管道类: ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 400 } - 配置调度器(使用组件中封装好的可以被共享的调度器) # 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用scrapy-redis组件自己的调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据 SCHEDULER_PERSIST = True - 指定存储数据的redis: REDIS_HOST = 'redis服务的ip地址' REDIS_PORT = 6379 - 配置redis数据库的配置文件 - 取消保护模式:protected-mode no - bind绑定: #bind 127.0.0.1 - 启动redis 6.执行分布式程序 scrapy runspider xxx.py 7.向调度器队列中仍入一个起始url: 在redis-cli中执行:
1.scrapy框架是否可以自己实现分布式?
- 不可以。原因有二。
其一:因为多台机器上部署的scrapy会各自拥有各自的调度器,这样就使得多台机器无法分配start_urls列表中的url。(多台机器无法共享同一个调度器)
其二:多台机器爬取到的数据无法通过同一个管道对数据进行统一的数据持久出存储。(多台机器无法共享同一个管道)
2.基于scrapy-redis组件的分布式爬虫
- scrapy-redis组件中为我们封装好了可以被多台机器共享的调度器和管道,我们可以直接使用并实现分布式数据爬取。
- 实现方式:
1.基于该组件的RedisSpider类
2.基于该组件的RedisCrawlSpider类
3.分布式实现流程:上述两种不同方式的分布式实现流程是统一的
- 3.1 下载scrapy-redis组件:pip install scrapy-redis
- 3.2 redis配置文件的配置:
3.3 修改爬虫文件中的相关代码:
- 将爬虫类的父类修改成基于RedisSpider或者RedisCrawlSpider。注意:如果原始爬虫文件是基于Spider的,则应该将父类修改成RedisSpider,如果原始爬虫文件是基于CrawlSpider的,则应该将其父类修改成RedisCrawlSpider。
- 注释或者删除start_urls列表,切加入redis_key属性,属性值为scrpy-redis组件中调度器队列的名称
3.4 在settings.py配置文件中进行相关配置,开启使用scrapy-redis组件中封装好的管道
3.5 在settings.py配置文件中进行相关配置,开启使用scrapy-redis组件中封装好的调度器
3.6 在settings.py配置文件中进行爬虫程序链接redis的配置:
3.7 开启redis服务器:redis-server 配置文件
3.8 开启redis客户端:redis-cli
3.9 运行爬虫文件:scrapy runspider SpiderFile.py
3.10 向调度器队列中扔入一个起始url(在redis客户端中操作):lpush redis_key属性值 起始url
需求:分布式爬取抽屉网中的标题(存储到redis中)
爬虫文件 chouti.py
# -*- coding: utf-8 -*- import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from scrapy_redis.spiders import RedisCrawlSpider from fenbushi_choutiPro.items import FenbushiChoutiproItem class ChoutiSpider(RedisCrawlSpider): name = 'chouti' # allowed_domains = ['www.xxx.com'] # start_urls = ['http://www.xxx.com/'] redis_key = "chouti" #调度器队列的名称 rules = ( Rule(LinkExtractor(allow=r'/all/hot/recent/\d+'), callback='parse_item', follow=True), ) def parse_item(self, response): div_list = response.xpath('//div[@class="news-content"]') for div in div_list: author = div.xpath('./div[2]/a[4]/b/text()').extract_first() title = div.xpath('./div[1]/a/text()').extract_first() item = FenbushiChoutiproItem() item["author"] = author item["title"] = title print('===>>',item["author"]) yield item
- items.py
import scrapy class FenbushiChoutiproItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() title = scrapy.Field() author = scrapy.Field()
- settings.py
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36' # Obey robots.txt rules ROBOTSTXT_OBEY = False
分布式的启动:
scrapy runspider chouti.py #找对路径 redis-server #记住要修改配置文件,具体修改见前面所说 redis-cli lpush chouti https://dig.chouti.com/ # 其中chouti指的是redis_key中内容
增量式爬虫
引言:
当我们在浏览相关网页的时候会发现,某些网站定时会在原有网页数据的基础上更新一批数据,例如某电影网站会实时更新一批最近热门的电影。小说网站会根据作者创作的进度实时更新最新的章节数据等等。那么,类似的情景,当我们在爬虫的过程中遇到时,我们是不是需要定时更新程序以便能爬取到网站中最近更新的数据呢?
一.增量式爬虫
- 概念:通过爬虫程序监测某网站数据更新的情况,以便可以爬取到该网站更新出的新数据。
- 如何进行增量式的爬取工作:
- 在发送请求之前判断这个URL是不是之前爬取过
- 在解析内容后判断这部分内容是不是之前爬取过
- 写入存储介质时判断内容是不是已经在介质中存在
- 分析:
不难发现,其实增量爬取的核心是去重, 至于去重的操作在哪个步骤起作用,只能说各有利弊。在我看来,前两种思路需要根据实际情况取一个(也可能都用)。第一种思路适合不断有新页面出现的网站,比如说小说的新章节,每天的最新新闻等等;第二种思路则适合页面内容会更新的网站。第三个思路是相当于是最后的一道防线。这样做可以最大程度上达到去重的目的。
- 分析:
- 去重方法
- 将爬取过程中产生的url进行存储,存储在redis的set中。当下次进行数据爬取时,首先对即将要发起的请求对应的url在存储的url的set中做判断,如果存在则不进行请求,否则才进行请求。
- 对爬取到的网页内容进行唯一标识的制定,然后将该唯一表示存储至redis的set中。当下次爬取到网页数据的时候,在进行持久化存储之前,首先可以先判断该数据的唯一标识在redis的set中是否存在,在决定是否进行持久化存储。
需求:爬取4567tv网站中所有的电影详情数据。(有更新的url时)
- 爬虫文件 movie.py
# -*- coding: utf-8 -*- import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from redis import Redis from increment1_move_Pro.items import Increment1MoveProItem class MoveSpider(CrawlSpider): name = 'movie' # allowed_domains = ['www.xxx.com'] start_urls = ['https://www.4567tv.tv/index.php/vod/show/id/7.html'] #起始url # 解析出其他页码中的电影数据 rules = ( Rule(LinkExtractor(allow=r'/index.php/vod/show/id/7/page/\d+.html'), callback='parse_item', follow=True), ) def parse_item(self, response): li_list = response.xpath('//li[@class="col-md-6 col-sm-4 col-xs-3"]') conn = Redis(host="127.0.0.1",port=6379)
for li in li_list: #获取详情页的数据 detail_url = 'https://www.4567tv.tv' + li.xpath('./div/a/@href').extract_first() ex = conn.sadd("movie_url",detail_url) #redis中添加set集合,有去重作用,如果已经存在会返回0,没有存在会添加并返回1 if ex == 1: yield scrapy.Request(url=detail_url,callback=self.parse_detail) #为1,说明是新添加的,然后去解析数据即可 else: print("没有要更新的数据") def parse_detail(self,response): #解析数据 title = response.xpath('/html/body/div[1]/div/div/div/div[2]/h1/text()').extract_first() actor = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[3]/span/text()').extract_first()
item = Increment1MoveProItem() item["title"] = title item["actor"] = actor yield item
- items.py
import scrapy class Increment1MoveProItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() title = scrapy.Field() actor = scrapy.Field()
- 管道文件 pipelines.py
from redis import Redis class Increment1MoveProPipeline(object): conn = None def open_spider(self,spider): #该方法只被调用一次 self.conn = Redis(host="127.0.0.1",port=6379) def process_item(self, item, spider): #可以被调用多次 print("爬取的数据正在入库......") self.conn.lpush("movie_data",item.__dict__) # 将解析到的数据存入redis中 lpush是添加列表,可以直接使用item.__dict__存储 return item
- 配置文件 settings.py
使用管道要在settings中添加
ITEM_PIPELINES = { 'increment1_move_Pro.pipelines.Increment1MoveProPipeline': 300, } USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36' # Obey robots.txt rules ROBOTSTXT_OBEY = False
需求:爬取糗事百科中的段子和作者数据。(有更新的内容,同一个url)
比如新闻和段子都是实时更新的,如果想要获取最新的内容,可以采用以下的方法
将内容进行哈希,存入redis中的集合,因为集合是不重复的,
如果redis中之前没有,会添加到redis中并返回1
如果之前就有了,那么会返回0,不做什么操作
- 爬虫文件:qiubai.py
# -*- coding: utf-8 -*- import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from increment2_Pro.items import Increment2ProItem from redis import Redis import hashlib class QiubaiSpider(CrawlSpider): name = 'qiubai' # allowed_domains = ['www.xxx.com'] start_urls = ['https://www.qiushibaike.com/text/'] rules = ( Rule(LinkExtractor(allow=r'/text/page/\d+/'), callback='parse_item', follow=True), #获取其他页码 ) def parse_item(self, response): div_list = response.xpath('//div[@class="article block untagged mb15 typs_hot"]') conn = Redis(host='127.0.0.1',port=6379) for div in div_list: item = Increment2ProItem() item['content'] = div.xpath('.//div[@class="content"]/span//text()').extract() item['content'] = ''.join(item['content']) item['author'] = div.xpath('./div/a[2]/h2/text() | ./div[1]/span[2]/h2/text()').extract_first() source = item['author']+item['content'] #自己制定了一种形式的数据指纹 hashValue = hashlib.sha256(source.encode()).hexdigest() ex = conn.sadd('qiubai_hash',hashValue) if ex == 1: yield item else: print('没有更新数据可爬!!!')
- items.py
import scrapy class Increment2ProItem(scrapy.Item): # define the fields for your item here like: content = scrapy.Field() author = scrapy.Field()
- 管道文件:pipelines.py
from redis import Redis class Increment2ProPipeline(object): conn = None def open_spider(self,spider): self.conn = Redis(host='127.0.0.1',port=6379) def process_item(self, item, spider): dic = { 'author':item['author'], 'content':item['content'] } self.conn.lpush('qiubaiData',dic) # self.conn.lpush('qiubaiDta',item.__dict__) #也可以直接使用 __dict__属性 print('爬取到一条数据,正在入库......') return item
- 配置文件 settings.py
使用管道要在settings中添加
ITEM_PIPELINES = { 'increment1_move_Pro.pipelines.Increment1MoveProPipeline': 300, } USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36' # Obey robots.txt rules ROBOTSTXT_OBEY = False
================