一 . crawlSpider
1. 上次了一种爬取全站数据是基于Scrapy框架中的Spider的递归爬取进行实现(Requests模块递归回调parse方法).
2. 现在在讲介绍一种比较好用的方法:基于CrawlSpider的自动爬取进行实现(更加的简洁高效).
crawlSpider的简介
CrawlSpider其实是Spider的一个子类,除了继承到Spider的特性和功能外,还派生除了其自己独有的更加强大的特性和功能。
其中最显著的功能就是”LinkExtractors链接提取器“。Spider是所有爬虫的基类,其设计原则只是为了爬取start_url列表中网页,
而从爬取到的网页中提取出的url进行继续的爬取工作使用CrawlSpider更合适。
scrawlSpider的使用
1.创建scrapy工程:scrapy startproject projectName
2.创建爬虫文件:scrapy genspider -t crawl spiderName www.xxx.com
--此指令对比以前的指令多了 "-t crawl",表示创建的爬虫文件是基于CrawlSpider这个类的,而不再是Spider这个基类。
看一下生成的爬虫文件
1 # -*- coding: utf-8 -*-
2 import scrapy
3 from scrapy.linkextractors import LinkExtractor
4 from scrapy.spiders import CrawlSpider, Rule
5
6
7 class SuperSpiderSpider(CrawlSpider):
8 name = 'super_spider'
9 allowed_domains = ['www.xxx.com']
10 start_urls = ['http://www.xxx.com/']
11
12 rules = (
13 Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
14 )
15
16 def parse_item(self, response):
17 item = {}
18 #item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
19 #item['name'] = response.xpath('//div[@id="name"]').get()
20 #item['description'] = response.xpath('//div[@id="description"]').get()
21 return item
-- 2,3,4行: 导入CrawlSpider相关模块
-- 7行: 表示该爬虫程序是基于CrawlSpider类的
-- 12,13,14行: 表示提取link规则
-- 16行: 解析方法
CrawlSpider类和Spider类的最大不同是CrawlSpider多了一个rules属性,其作用是定义”提取动作“。
在rules中可以包含一个或多个Rule对象,在Rule对象中包含了LinkExtractor对象。
LinkExtrator:链接提取器
LinkExtractor(
allow=r'Items/',# 满足括号中“正则表达式”的值会被提取,如果为空,则全部匹配。
deny=xxx, # 满足正则表达式的则不会被提取。
restrict_xpaths=xxx, # 满足xpath表达式的值会被提取
restrict_css=xxx, # 满足css表达式的值会被提取
deny_domains=xxx, # 不会被提取的链接的domains。
)
# 作用:提取response中符合规则的链接。
Rule : 规则解析器。根据链接提取器中提取到的链接,根据指定规则提取解析器链接网页中的内容.
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True)
-- 参数介绍:
参数1:指定链接提取器
参数2:指定规则解析器解析数据的规则(回调函数)
参数3:是否将链接提取器继续作用到链接提取器提取出的链接网页中。当callback为None,参数3的默认值为true。
rules=( ):指定不同规则解析器。一个Rule对象表示一种提取规则。
CrawlSpider整体爬取流程:
a)爬虫文件首先根据起始url,获取该url的网页内容
b)链接提取器会根据指定提取规则将步骤a中网页内容中的链接进行提取
c)规则解析器会根据指定解析规则将链接提取器中提取到的链接中的网页内容根据指定的规则进行解析
d)将解析数据封装到item中,然后提交给管道进行持久化存储
话不多说, 上代码
# 爬取糗事百科糗图板块的所有页码数据
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class CrawldemoSpider(CrawlSpider):
name = 'qiubai'
#allowed_domains = ['www.qiushibaike.com']
start_urls = ['https://www.qiushibaike.com/pic/']
#连接提取器:会去起始url响应回来的页面中提取指定的url
link = LinkExtractor(allow=r'/pic/page/\d+\?') #s=为随机数
link1 = LinkExtractor(allow=r'/pic/$')#爬取第一页
#rules元组中存放的是不同的规则解析器(封装好了某种解析规则)
rules = (
#规则解析器:可以将连接提取器提取到的所有连接表示的页面进行指定规则(回调函数)的解析
Rule(link, callback='parse_item', follow=True),
Rule(link1, callback='parse_item', follow=True),
)
def parse_item(self, response):
print(response)
上面是牛刀小试,下边是一个完整的流程
爬虫文件
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from qiubaiBycrawl.items import QiubaibycrawlItem
import re
class QiubaitestSpider(CrawlSpider):
name = 'qiubaiTest'
#起始url
start_urls = ['http://www.qiushibaike.com/']
#定义链接提取器,且指定其提取规则
page_link = LinkExtractor(allow=r'/8hr/page/\d+/')
rules = (
#定义规则解析器,且指定解析规则通过callback回调函数
Rule(page_link, callback='parse_item', follow=True),
)
#自定义规则解析器的解析规则函数
def parse_item(self, response):
div_list = response.xpath('//div[@id="content-left"]/div')
for div in div_list:
#定义item
item = QiubaibycrawlItem()
#根据xpath表达式提取糗百中段子的作者
item['author'] = div.xpath('./div/a[2]/h2/text()').extract_first().strip('\n')
#根据xpath表达式提取糗百中段子的内容
item['content'] = div.xpath('.//div[@class="content"]/span/text()').extract_first().strip('\n')
yield item #将item提交至管道
items.py
# -*- coding: utf-8 -*-
# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html
import scrapy
class QiubaibycrawlItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
author = scrapy.Field() #作者
content = scrapy.Field() #内容
pipelines.py
# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
class QiubaibycrawlPipeline(object):
def __init__(self):
self.fp = None
def open_spider(self,spider):
print('开始爬虫')
self.fp = open('./data.txt','w')
def process_item(self, item, spider):
#将爬虫文件提交的item写入文件进行持久化存储
self.fp.write(item['author']+':'+item['content']+'\n')
return item
def close_spider(self,spider):
print('结束爬虫')
self.fp.close()
二 . 分布式爬虫
首先我们先考虑一个问题: scrapy框架是否可以自己实现分布式?
不可以。原因有二。
其一:因为多台机器上部署的scrapy会各自拥有各自的调度器,这样就使得多台机器无法分配start_urls列表中的url。(多台机器无法共享同一个调度器)
其二:多台机器爬取到的数据无法通过同一个管道对数据进行统一的数据持久出存储。(多台机器无法共享同一个管道)
基于scrapy-redis组件的分布式爬虫
scrapy-redis可以解决上述两个问题
scrapy-redis组件中为我们封装好了可以被多台机器共享的调度器和管道,我们可以直接使用并实现分布式数据爬取。
实现方式:
1.基于该组件的RedisSpider类
2.基于该组件的RedisCrawlSpider类
分布式实现流程
1.下载scrapy-redis组件:pip install scrapy-redis
2. redis配置文件的配置:
- 注释该行:bind 127.0.0.1,表示可以让其他ip访问redis
- 将yes该为no:protected-mode no,表示可以让其他ip操作redis
修改爬虫文件中的相关代码:
# 先导入包: from scrapy_redis.spiders import RedisCrawlSpider
- 将爬虫类的父类修改成基于RedisSpider或者RedisCrawlSpider。
注意:如果原始爬虫文件是基于Spider的,则应该将父类修改成RedisSpider,
如果原始爬虫文件是基于CrawlSpider的,则应该将其父类修改成RedisCrawlSpider。
- 注释或者删除start_urls列表,且加入redis_key属性,属性值为scrpy-redis组件中调度器队列的名称
在配置文件中进行相关配置,开启使用scrapy-redis组件中封装好的管道
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 400 # 直接复制粘贴就行
}
在配置文件中进行相关配置,开启使用scrapy-redis组件中封装好的调度器
# 使用scrapy-redis组件的去重队列
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis组件自己的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 是否允许暂停
SCHEDULER_PERSIST = True
在配置文件中进行爬虫程序链接redis的配置
REDIS_HOST = 'redis服务的ip地址'
REDIS_PORT = 6379
1. 开启redis服务器:redis-server 配置文件
2. 开启redis客户端:redis-cli
3. 运行爬虫文件:scrapy runspider SpiderFile
4. 向调度器队列中扔入一个起始url(在redis客户端中操作):lpush redis_key属性值 起始url
三 . 增量式爬虫
什么是增量是爬虫?
说白了就是你爬完一个网站的数据后,他又更新了新的数据,而你不需要重新全爬一边,只需要把更新的数据爬下来就可以啦,这就是增量式爬虫!
如何进行增量式爬取工作呢?
第一种方法:在发送请求之前判断这个URL是不是之前爬取过
第二种方法:在解析内容后判断这部分内容是不是之前爬取过
第三种方法:写入存储数据库时判断内容是不是已经在数据库中存在
实现上述方法的核心其实就是去重
第一种方法适合不断有新网页出现的网站,比如小说的新章节,每天最新的新闻等等
第二种方法适合内容更新的网站
第三种方法是最大程度上去重
去重方法
1.将爬取过程中产生的url进行存储,存储在redis的set中。当下次进行数据爬取时,
首先对即将要发起的请求对应的url在存储的url的set中做判断,如果存在则不进行请求,否则才进行请求。
2.对爬取到的网页内容进行唯一标识的制定,然后将该唯一表示存储至redis的set中。
当下次爬取到网页数据的时候,在进行持久化存储之前,首先可以先判断该数据的唯一标识在redis的set中是否存在,在决定是否进行持久化存储。
案例1: 爬取4567tv网站中所有的电影详情数据。(基于url是否重复)
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from redis import Redis
from incrementPro.items import IncrementproItem
class MovieSpider(CrawlSpider):
name = 'movie'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://www.4567tv.tv/frim/index7-11.html']
rules = (
Rule(LinkExtractor(allow=r'/frim/index7-\d+\.html'), callback='parse_item', follow=True),
)
#创建redis链接对象
conn = Redis(host='127.0.0.1',port=6379)
def parse_item(self, response):
li_list = response.xpath('//li[@class="p1 m1"]')
for li in li_list:
#获取详情页的url
detail_url = 'http://www.4567tv.tv'+li.xpath('./a/@href').extract_first()
#将详情页的url存入redis的set中
ex = self.conn.sadd('urls',detail_url)
if ex == 1:
print('该url没有被爬取过,可以进行数据的爬取')
yield scrapy.Request(url=detail_url,callback=self.parst_detail)
else:
print('数据还没有更新,暂无新数据可爬取!')
#解析详情页中的电影名称和类型,进行持久化存储
def parst_detail(self,response):
item = IncrementproItem()
item['name'] = response.xpath('//dt[@class="name"]/text()').extract_first()
item['kind'] = response.xpath('//div[@class="ct-c"]/dl/dt[4]//text()').extract()
item['kind'] = ''.join(item['kind'])
yield item
pipelines.py
# -*- coding: utf-8 -*-
from redis import Redis
class IncrementproPipeline(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 = {
'name':item['name'],
'kind':item['kind']
}
print(dic)
self.conn.lpush('movieData',dic)
return item
案例2: 爬取糗事百科中的段子和作者数据。(基于内容的唯一标识)
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from incrementByDataPro.items import IncrementbydataproItem
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),
Rule(LinkExtractor(allow=r'/text/$'), callback='parse_item', follow=True),
)
#创建redis链接对象
conn = Redis(host='127.0.0.1',port=6379)
def parse_item(self, response):
div_list = response.xpath('//div[@id="content-left"]/div')
for div in div_list:
item = IncrementbydataproItem()
item['author'] = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span[2]/h2/text()').extract_first()
item['content'] = div.xpath('.//div[@class="content"]/span/text()').extract_first()
#将解析到的数据值生成一个唯一的标识进行redis存储
source = item['author']+item['content']
source_id = hashlib.sha256(source.encode()).hexdigest()
#将解析内容的唯一表示存储到redis的data_id中
ex = self.conn.sadd('data_id',source_id)
if ex == 1:
print('该条数据没有爬取过,可以爬取......')
yield item
else:
print('该条数据已经爬取过了,不需要再次爬取了!!!')
pipelines.py
# -*- coding: utf-8 -*-
from redis import Redis
class IncrementbydataproPipeline(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']
}
# print(dic)
self.conn.lpush('qiubaiData', dic)
return item