爬虫之scrapy、scrapy-redis

一、Scrapy的简介

  Scrapy 是基于twisted框架开发而来,twisted是一个流行的事件驱动的python网络框架。因此Scrapy使用了一种非阻塞(又名异步)的代码来实现并发。整体架构大致如下

它主要由五大组件和两中间件组成: 

五大组件:

1、引擎(ENGINE):用来处理整个系统的数据流处理, 触发事务(框架核心)

2、调度器(SCHEDULER) : 用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址

3、下载器(DOWLOADER):用于下载网页内容, 并将网页内容返回给ENGINE,下载器是建立在twisted这个高效的异步模型上的

4、爬虫(Spiders):爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面

5、项目管道(Pipeline):负责处理爬虫从网页中抽取的实体(item),主要的功能是对item进行持久化、验证有效性、清理。当页面被操作。

两大中间件:

1、下载中间件:位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应。比如:添加请求头,更换user-agent、代理ip等

2、爬虫中间件:介于Scrapy引擎和spiders之间的框架,主要工作是处理spider的响应输入和请求输出。

二、Scrapy的使用

1、安装

#Windows平台
    1、pip3 install wheel #安装后,便支持通过wheel文件安装软件,wheel文件官网:https://www.lfd.uci.edu/~gohlke/pythonlibs
    3、pip3 install lxml
    4、pip3 install pyopenssl
    5、下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/
    6、下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
    7、执行pip3 install 下载目录\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
    8、pip3 install scrapy

#Linux平台
    1、pip3 install scrapy

2、Scrapy工程的基本操作以及命令

#注意:都是在cmd下进行命令操作
# 1、创建scrapy工程
scrapy startproject 工程名字

# 2、切换到scrapy攻略目录下,创建爬虫文件
 scrapy genspider 爬虫文件名字  www.xxx.com(允许爬取的域名)

# 3、执行scrapy工程
scrapy crawl 爬虫文件名字(不用带.py)

# 也可以创建一个py文件,内容如下,每次执行该py文件就可以
from scrapy.cmdline import execute
execute(['scrapy','crawl','爬虫文件名'])
'''
用法:  scrapy <命令> [options] [args]

可用命令:
  bench         # scrapy压力测试
  check         # 检测项目有无语法错误 
  commands
  crawl          # 运行爬虫
  edit            # 编辑器,一般不用
  fetch          #独立于程单纯地爬取一个页面,可以拿到请求头
  genspider    #创建爬虫程序
  list              #列出项目中所包含的爬虫名
  parse          #scrapy parse url地址 --callback 回调函数  #以此可以验证我们的回调函数是否正确
  runspider     #运行一个独立的python文件,不必创建项目
  settings       #如果是在项目目录下,则得到的是该项目的配置
  shell            #scrapy shell url地址  在交互式调试,如选择器规则正确与否
  startproject  #创建项目
  version        #scrapy version 查看scrapy的版本,scrapy version -v查看scrapy依赖库的版本
  view           #下载完毕后直接弹出浏览器,以此可以分辨出哪些数据是ajax请求
'''
'''
例子:
注意:执行项目命令,要切到项目的目录下(check、list、parse、bench)
'''
scrapy settings --get XXX #如果切换到项目目录下,看到的则是该项目的配置

scrapy runspider baidu.py

scrapy shell https://www.baidu.com
    response
    response.status
    response.body
    view(response)
    
scrapy view https://www.taobao.com #如果页面显示内容不全,不全的内容则是ajax请求实现的,以此快速定位问题

scrapy fetch --nolog --headers https://www.taobao.com

scrapy version #scrapy的版本

scrapy version -v #依赖库的版本

3、项目结构

"""
├── project_name/
    ├── scrapy.cfg/    #项目的主配置信息,用来部署scrapy时使用,爬虫相关的配置信息在settings.py文件中
    ├── project_name/             
        ├── __init__.py/        
        ├── items.py/        # 设置数据存储模板,用于结构化数据,类似Django的Model    
        ├── settings.py      # 配置文件,如:递归的层数、并发数,延迟下载等。强调:配置文件的选项必须大写否则视为无效,正确写法USER_AGENT='xxxx'
        ├── pipelines.py/    # 数据处理行为,如:一般结构化的数据持久化
        ├── spiders/         # 爬虫目录,如:创建文件,编写爬虫规则
            ├── __init__.py/   
            ├── 爬虫文件1.py       # 项目开发时的本地配置
            ├── 爬虫文件2.py       # 项目开发时的本地配置
            ......
"""

4、数据解析

xpath解析和css解析

#1 //与/
response.xpath('//body/a/')#
response.css('div a::text')

response.xpath('//body/a') #开头的//代表从整篇文档中寻找,body之后的/代表body的儿子
>>> []
response.xpath('//body//a') #开头的//代表从整篇文档中寻找,body之后的//代表body的子子孙孙
>>> [<Selector xpath='//body//a' data='<a href="image1.html">Name: My image 1 <'>, <Selector xpath='//body//a' data='<a href="image2.html">Name: My image 2 <'>, <Selector xpath='//body//a' data='<a href="
image3.html">Name: My image 3 <'>, <Selector xpath='//body//a' data='<a href="image4.html">Name: My image 4 <'>, <Selector xpath='//body//a' data='<a href="image5.html">Name: My image 5 <'>]

#2 text 获取文本值
response.xpath('//body//a/text()')
response.css('body a::text')
'''
3、extract与extract_first:从selector对象中解出内容
extract()取出的是一个列表
extract_first()取出的是列表第一个元素
'''
response.xpath('//div/a/text()').extract()
>>> ['Name: My image 1 ', 'Name: My image 2 ', 'Name: My image 3 ', 'Name: My image 4 ', 'Name: My image 5 ']
response.css('div a::text').extract()
>>> ['Name: My image 1 ', 'Name: My image 2 ', 'Name: My image 3 ', 'Name: My image 4 ', 'Name: My image 5 ']

response.xpath('//div/a/text()').extract_first()
>>> 'Name: My image 1 '
response.css('div a::text').extract_first()
>>> 'Name: My image 1 '

#4、属性:xpath的属性加前缀@
response.xpath('//div/a/@href').extract_first()
>>> 'image1.html'
response.css('div a::attr(href)').extract_first()
>>> 'image1.html'

#4、嵌套查找
response.xpath('//div').css('a').xpath('@href').extract_first()
>>> 'image1.html'

#5、设置默认值
response.xpath('//div[@id="xxx"]').extract_first(default="not found")
>>> 'not found'

#4、按照属性查找
response.xpath('//div[@id="images"]/a[@href="image3.html"]/text()').extract()
response.css('#images a[@href="image3.html"]/text()').extract()

#5、按照属性模糊查找
response.xpath('//a[contains(@href,"image")]/@href').extract()
response.css('a[href*="image"]::attr(href)').extract()

response.xpath('//a[contains(@href,"image")]/img/@src').extract()
response.css('a[href*="imag"] img::attr(src)').extract()

response.xpath('//*[@href="image1.html"]')
response.css('*[href="image1.html"]')

#6、正则表达式
response.xpath('//a/text()').re(r'Name: (.*)')
response.xpath('//a/text()').re_first(r'Name: (.*)')

#7、xpath相对路径
res=response.xpath('//a[contains(@href,"3")]')[0]
res.xpath('img')
>>> [<Selector xpath='img' data='<img src="image3_thumb.jpg">'>]
res.xpath('./img')
>>> [<Selector xpath='./img' data='<img src="image3_thumb.jpg">'>]
res.xpath('.//img')
>>> [<Selector xpath='.//img' data='<img src="image3_thumb.jpg">'>]
res.xpath('//img') #这就是从头开始扫描
>>> [<Selector xpath='//img' data='<img src="image1_thumb.jpg">'>, <Selector xpath='//img' data='<img src="image2_thumb.jpg">'>, <Selector xpath='//img' data='<img src="image3_thumb.jpg">'>, <Selector xpa
th='//img' data='<img src="image4_thumb.jpg">'>, <Selector xpath='//img' data='<img src="image5_thumb.jpg">'>]

#8、带变量的xpath
response.xpath('//div[@id=$xxx]/a/text()',xxx='images').extract_first()
>>> 'Name: My image 1 '
response.xpath('//div[count(a)=$yyy]/@id',yyy=5).extract_first() #求有5个a标签的div的id
>>> 'images'

5、配置

# robosttxt 协议设置为false
ROBOTSTXT_OBEY = False
# 日志配置成错误级别
LOG_LEVEL='ERROR'
# 请求头配置
USER_AGENT = '浏览器标识'

'''
提升scrapy爬取数据效率的配置
#1 增加并发:
默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改CONCURRENT_REQUESTS = 100值为100,并发设置成了为100。
#2 降低日志级别:
在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:LOG_LEVEL = ‘INFO’
# 3 禁止cookie:
如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:COOKIES_ENABLED = False
# 4禁止重试:
对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:RETRY_ENABLED = False
# 5 减少下载超时:
如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:DOWNLOAD_TIMEOUT = 10 超时时间为10s
'''

'''
其他配置可以根据需要进行相应配置,也可以参考下面的其他配置
'''
#==>第一部分:基本配置<===
#1、项目名称,默认的USER_AGENT由它来构成,也作为日志记录的日志名
BOT_NAME = 'Amazon'

#2、爬虫应用路径
SPIDER_MODULES = ['Amazon.spiders']
NEWSPIDER_MODULE = 'Amazon.spiders'

#3、客户端User-Agent请求头
#USER_AGENT = 'Amazon (+http://www.yourdomain.com)'

#4、是否遵循爬虫协议
# Obey robots.txt rules
ROBOTSTXT_OBEY = False

#5、是否支持cookie,cookiejar进行操作cookie,默认开启
#COOKIES_ENABLED = False

#6、Telnet用于查看当前爬虫的信息,操作爬虫等...使用telnet ip port ,然后通过命令操作
#TELNETCONSOLE_ENABLED = False
#TELNETCONSOLE_HOST = '127.0.0.1'
#TELNETCONSOLE_PORT = [6023,]

#7、Scrapy发送HTTP请求默认使用的请求头
#DEFAULT_REQUEST_HEADERS = {
#   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
#   'Accept-Language': 'en',
#}



#===>第二部分:并发与延迟<===
#1、下载器总共最大处理的并发请求数,默认值16
#CONCURRENT_REQUESTS = 32

#2、每个域名能够被执行的最大并发请求数目,默认值8
#CONCURRENT_REQUESTS_PER_DOMAIN = 16

#3、能够被单个IP处理的并发请求数,默认值0,代表无限制,需要注意两点
#I、如果不为零,那CONCURRENT_REQUESTS_PER_DOMAIN将被忽略,即并发数的限制是按照每个IP来计算,而不是每个域名
#II、该设置也影响DOWNLOAD_DELAY,如果该值不为零,那么DOWNLOAD_DELAY下载延迟是限制每个IP而不是每个域
#CONCURRENT_REQUESTS_PER_IP = 16

#4、如果没有开启智能限速,这个值就代表一个规定死的值,代表对同一网址延迟请求的秒数
#DOWNLOAD_DELAY = 3

#===>第三部分:智能限速/自动节流:AutoThrottle extension<===
#一:介绍
from scrapy.contrib.throttle import AutoThrottle #http://scrapy.readthedocs.io/en/latest/topics/autothrottle.html#topics-autothrottle
设置目标:
1、比使用默认的下载延迟对站点更好
2、自动调整scrapy到最佳的爬取速度,所以用户无需自己调整下载延迟到最佳状态。用户只需要定义允许最大并发的请求,剩下的事情由该扩展组件自动完成

#二:如何实现?
在Scrapy中,下载延迟是通过计算建立TCP连接到接收到HTTP包头(header)之间的时间来测量的。
注意,由于Scrapy可能在忙着处理spider的回调函数或者无法下载,因此在合作的多任务环境下准确测量这些延迟是十分苦难的。 不过,这些延迟仍然是对Scrapy(甚至是服务器)繁忙程度的合理测量,而这扩展就是以此为前提进行编写的。

#三:限速算法
自动限速算法基于以下规则调整下载延迟
#1、spiders开始时的下载延迟是基于AUTOTHROTTLE_START_DELAY的值
#2、当收到一个response,对目标站点的下载延迟=收到响应的延迟时间/AUTOTHROTTLE_TARGET_CONCURRENCY
#3、下一次请求的下载延迟就被设置成:对目标站点下载延迟时间和过去的下载延迟时间的平均值
#4、没有达到200个response则不允许降低延迟
#5、下载延迟不能变的比DOWNLOAD_DELAY更低或者比AUTOTHROTTLE_MAX_DELAY更高

#四:配置使用
#开启True,默认False
AUTOTHROTTLE_ENABLED = True
#起始的延迟
AUTOTHROTTLE_START_DELAY = 5
#最小延迟
DOWNLOAD_DELAY = 3
#最大延迟
AUTOTHROTTLE_MAX_DELAY = 10
#每秒并发请求数的平均值,不能高于 CONCURRENT_REQUESTS_PER_DOMAIN或CONCURRENT_REQUESTS_PER_IP,调高了则吞吐量增大强奸目标站点,调低了则对目标站点更加”礼貌“
#每个特定的时间点,scrapy并发请求的数目都可能高于或低于该值,这是爬虫视图达到的建议值而不是硬限制
AUTOTHROTTLE_TARGET_CONCURRENCY = 16.0
#调试
AUTOTHROTTLE_DEBUG = True
CONCURRENT_REQUESTS_PER_DOMAIN = 16
CONCURRENT_REQUESTS_PER_IP = 16



#===>第四部分:爬取深度与爬取方式<===
#1、爬虫允许的最大深度,可以通过meta查看当前深度;0表示无深度
# DEPTH_LIMIT = 3

#2、爬取时,0表示深度优先Lifo(默认);1表示广度优先FiFo

# 后进先出,深度优先
# DEPTH_PRIORITY = 0
# SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleLifoDiskQueue'
# SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.LifoMemoryQueue'
# 先进先出,广度优先

# DEPTH_PRIORITY = 1
# SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleFifoDiskQueue'
# SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.FifoMemoryQueue'

#3、调度器队列
# SCHEDULER = 'scrapy.core.scheduler.Scheduler'
# from scrapy.core.scheduler import Scheduler

#4、访问URL去重
# DUPEFILTER_CLASS = 'step8_king.duplication.RepeatUrl'



#===>第五部分:中间件、Pipelines、扩展<===
#1、Enable or disable spider middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
#    'Amazon.middlewares.AmazonSpiderMiddleware': 543,
#}

#2、Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
   # 'Amazon.middlewares.DownMiddleware1': 543,
}

#3、Enable or disable extensions
# See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
#EXTENSIONS = {
#    'scrapy.extensions.telnet.TelnetConsole': None,
#}

#4、Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
   # 'Amazon.pipelines.CustomPipeline': 200,
}



#===>第六部分:缓存<===
"""

1. 启用缓存
   目的用于将已经发送的请求或相应缓存下来,以便以后使用

   from scrapy.downloadermiddlewares.httpcache import HttpCacheMiddleware
   from scrapy.extensions.httpcache import DummyPolicy
   from scrapy.extensions.httpcache import FilesystemCacheStorage
   """
   # 是否启用缓存策略
   # HTTPCACHE_ENABLED = True

# 缓存策略:所有请求均缓存,下次在请求直接访问原来的缓存即可
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
# 缓存策略:根据Http响应头:Cache-Control、Last-Modified 等进行缓存的策略
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.RFC2616Policy"

# 缓存超时时间
# HTTPCACHE_EXPIRATION_SECS = 0

# 缓存保存路径
# HTTPCACHE_DIR = 'httpcache'

# 缓存忽略的Http状态码
# HTTPCACHE_IGNORE_HTTP_CODES = []

# 缓存存储的插件
# HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

#===>第七部分:线程池<===
REACTOR_THREADPOOL_MAXSIZE = 10

#Default: 10
#scrapy基于twisted异步IO框架,downloader是多线程的,线程数是Twisted线程池的默认大小(The maximum limit for Twisted Reactor thread pool size.)

#关于twisted线程池:
http://twistedmatrix.com/documents/10.1.0/core/howto/threading.html

#线程池实现:twisted.python.threadpool.ThreadPool
twisted调整线程池大小:
from twisted.internet import reactor
reactor.suggestThreadPoolSize(30)
其他配置

6、scrapy持久化存储

(1)基于终端指令

'''
要求:只可以将parse方法的返回值存储到本地的文本文件中
注意:持久化存储对应的文本文件的类型只可以为:‘json','jsonlines','jl',’csv',‘xml',’marshal','pickle‘

指令:scrapy crawl 爬虫文件名 -o filePath

优点:简介高效便捷
缺点:局限性比较强(数据只可以存储到指定后缀的文本文件中)
'''

(2)基于管道

'''
items.py: 数据结构模版文件,定义数据属性
pipelines.py:管道文件,用来接收数据(items),进行持久化操作

持久化流程:
1.爬虫文件爬取到数据后,需要将数据封装到items对象中。需要导入items.py文件里面的类
2.使用yield关键字将items对象提交给pipelines管道进行持久化操作。
3.在管道文件中的process_item方法中接收爬虫文件提交过来的item对象,然后编写持久化存储的代码将item对象中存储的数据进行持久化存储
4.settings.py配置文件中开启管道
'''
# pipline.py 管道类
class BlogPostPipeline:
    # 开始
    def open_spider(self,spider):
        self.conn=pymysql.connect(host='127.0.0.1', user='root', password="root",database='pcdb', charset='utf8mb4',port=3306,)
    # 持久化过程
    def process_item(self,item,spider):
        cursor=self.conn.cursor()
        sql = 'insert into article (title,url,content,`desc`) values (%s,%s,%s,%s)'
        cursor.execute(sql, [item['title'], item['url'], item['content'], item['desc']])
        self.conn.commit()
        return item
    # 结束
    def close_spider(self,spider):
        self.conn.close()
        print('爬虫结束了')


#items文件
import scrapy
class PostItem(scrapy.Item):

    title=scrapy.Field()
    url=scrapy.Field()
    desc=scrapy.Field()
    content=scrapy.Field()

图片存储 :

1、在setting文件配置:IMAGES_STORE = ‘./imgs’:表示最终图片存储的目录,并在ITEM_PIPELINES中增加管道类中新增的类

2、重写一个管道类(继承ImagesPipeline)

3、基于ImagesPipeLine类的管道类三个方法:get_media_request()、file_path()、tiem_completed()

from scrapy.pipelines.images import ImagesPipeline
# 继承ImagesPipeline
class myPipeline(ImagesPipeline):
  
    def get_media_requests(self, item, info):
        print('下载开始')
        return scrapy.Request(item['img_url'],headers={'referer':item['img_referer']},meta={'item':item})

    def file_path(self, request, response=None, info=None):
        item=request.meta['item']
        url = request.url
        file_name=url.split('/')[-1]
        return file_name

    def item_completed(self, results, item, info):
        print('下载结束')
        return item

7、scrapy中间件(下载中间件)

(1)拦截请求:修改user-agent、请求头,加入代理ip(可以方法process_exception方法,或者process_request方法)等

(2)拦截响应:修改响应对象或者响应数据(例子:比如一些动态加载的页面)

 def process_request(self, request, spider):
       #1、更改请求头的user-agent
        from fake_useragent import UserAgent
        ua=UserAgent()
        request.headers['User-Agent']=ua.random
       # 2、设置代理ip
        import requests
        r = requests.get('http://127.0.0.1:5010/get/')
        proxy=r.json().get('proxy')
        request.meta['download_timeout'] = 5
        request.meta["proxy"] = 'http://'+proxy
        print(request)
        return None
# 在中间件使用selenium处理ajax请求问题
 def process_response(self, request, response, spider):

        from scrapy.http import HtmlResponse

        spider.bro.get('https://dig.chouti.com/')

        response = HtmlResponse(url='https://dig.chouti.com/', body=spider.bro.page_source.encode('utf-8'),
                                request=request)

        return response

三、去重源码解析

1、BaseDupeFilter源码

class BaseDupeFilter:
    @classmethod
    def from_settings(cls, settings):
        return cls()
    # 去重方法
    def request_seen(self, request):
        return False
    def open(self):  # can return deferred
        pass
    def close(self, reason):  # can return a deferred
        pass
    def log(self, request, spider):  # log that a request has been filtered
        pass

2、RFPDupeFilter源码

class RFPDupeFilter(BaseDupeFilter):
    """Request Fingerprint duplicates filter"""

    def __init__(self, path=None, debug=False):
        self.file = None
        self.fingerprints = set() #集合
        self.logdupes = True
        self.debug = debug
        self.logger = logging.getLogger(__name__)
        if path:
            self.file = open(os.path.join(path, 'requests.seen'), 'a+')
            self.file.seek(0)
            self.fingerprints.update(x.rstrip() for x in self.file)

    @classmethod
    def from_settings(cls, settings):
        debug = settings.getbool('DUPEFILTER_DEBUG')
        return cls(job_dir(settings), debug)
    # 去重的主要方法
    def request_seen(self, request):
        '''
        from scrapy.utils.request import request_fingerprint
        利用request_fingerprint对请求取指纹(md5)
        判断如果在集合中,返回True,不在则添加到集合
        '''
        fp = self.request_fingerprint(request)
        if fp in self.fingerprints:
            return True
        self.fingerprints.add(fp)
        if self.file:
            self.file.write(fp + '\n')

    def request_fingerprint(self, request):
        return request_fingerprint(request)

    def close(self, reason):
        if self.file:
            self.file.close()

    def log(self, request, spider):
        if self.debug:
            msg = "Filtered duplicate request: %(request)s (referer: %(referer)s)"
            args = {'request': request, 'referer': referer_str(request)}
            self.logger.debug(msg, args, extra={'spider': spider})
        elif self.logdupes:
            msg = ("Filtered duplicate request: %(request)s"
                   " - no more duplicates will be shown"
                   " (see DUPEFILTER_DEBUG to show all duplicates)")
            self.logger.debug(msg, {'request': request}, extra={'spider': spider})
            self.logdupes = False

        spider.crawler.stats.inc_value('dupefilter/filtered', spider=spider)

3、通过BaseDupeFilter和RFPDupeFilter对比,我们可以看出去重规则主要在request_seen()方法中,则我们可以仿照写一个自定义去重类

(1) 新建一个去重文件dupfilter.py

from scrapy.dupefilters import BaseDupeFilter
class UrlFilter(BaseDupeFilter):
    def __init__(self):
        self.visited = set() #或者放到数据库

    @classmethod
    def from_settings(cls, settings):
        return cls()

   def request_seen(self, request):
        if request.url in self.visited:
            return True
        self.visited.add(request.url)

    def open(self):  # can return deferred
         pass

    def close(self, reason):  # can return a deferred
         pass

    def log(self, request, spider):  # log that a request has been filtered
         pass

(2) 在配置文件settings中

DUPEFILTER_CLASS = '项目名.dupfilter.UrlFilter'

四、scrapy-redis分布式爬虫

1、介绍

scrapy-redis是在原来的scrapy的基础上,重写Scheduler,让调度器到共享队列中取Request(该request请求时去重的)。去重主要是利用了redis的集合类型

 

 

 2、使用方法

(1)安装scrapy-redis模块,pip install scrapy-redis

(2)配置settings文件

# scheduler调度器配置,参考Scheduler源码
class Scheduler(object):
    """
    在scrapy中settings的配置信息
    --------
    SCHEDULER_PERSIST : bool (default: False)
        关闭的时候,是否保留redis队列(也就是起始地址).
    SCHEDULER_FLUSH_ON_START : bool (default: False)
        是否在启动时刷新redis队列。
    SCHEDULER_IDLE_BEFORE_CLOSE : int (default: 0)
        如果没有收到任何消息,在关闭之前要等待多少秒(超时时间)。
    SCHEDULER_QUEUE_KEY : str
        调度器中请求存放在redis中的key
    SCHEDULER_QUEUE_CLASS : str
        调度程序队列类s.
    SCHEDULER_DUPEFILTER_KEY : str
        去重规则中,在redis中保存对应的key值.
    SCHEDULER_DUPEFILTER_CLASS : str
        去重规则对应的处理类
    SCHEDULER_SERIALIZER : str
        对保存到redis中的数据进行序列化.默认使用pickle
        # SCHEDULER_SERIALIZER ="scrapy_redis.picklecompat"

    """

# Redis配置,参考connection源码
def get_redis_from_settings(settings):
   # ----------------
   # 在settings中的配置
   # ----------------
    REDIS_URL : str, optional
        # 例子:REDIS_URL='redis://:12345@127.0.0.1:6379'
        #redis的连接url,优先去url配置(等同的host\post的配置),.
        #注意:配置了url,就不用配置主机号跟端口了
    REDIS_HOST : str, optional
        #redis的主机名.
    REDIS_PORT : str, optional
        #redis的端口.
    REDIS_ENCODING : str, optional
        #redis的编码.
    REDIS_PARAMS : dict, optional
        #redis的其他参数,是字典类型.
        #如: REDIS_PARAMS = {'password':'12345'}
  
# 持久化 配置,参考pipeline源码
class RedisPipeline(object):

    Settings
    --------
    REDIS_ITEMS_KEY : str
        # redis关键字
    REDIS_ITEMS_SERIALIZER : str
        # 序列化函数


# 参考spiders源码配置
 class RedisSpider(RedisMixin, Spider):
    Settings
    --------
    REDIS_START_URLS_KEY : str (default: "<spider.name>:start_urls")
        # 起始rurl从对应的redis的key.
    REDIS_START_URLS_BATCH_SIZE : int (deprecated by CONCURRENT_REQUESTS)
        # 每次尝试从reids获取的url数,默认从配置中获取.
    REDIS_START_URLS_AS_SET : bool (default: False)
        # 获取起始URL时,如果为True,则使用self.server.spop;如果为False,则使用self.server.lpop
    REDIS_ENCODING : str (default: "utf-8")
        #编码

(3) 参考案例(在原来的scrapy的源码上进行修改)

'''
spdies文件
继承RedisSpider
'''
from scrapy_redis.spiders import RedisSpider

class BlogSpider(RedisSpider):
    name = 'redis_blog'
    redis_key = 'myspider:start_urls'
    def parse(self, response):
        print('---redis-blog-----')
        article_list = response.xpath('//div[@id="post_list"]/article')
        ....
#settings文件配置
#redis的连接
REDIS_HOST='localhost'
REDIS_PORT=6379
# REDIS_PASSWD='12345'
REDIS_PARAMS = {'password':'12345'}
# REDIS_URL='redis://:12345@127.0.0.1:6379'
# from scrapy_redis.scheduler import Scheduler
DUPEFILTER_CLASS="scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER = 'scrapy_redis.scheduler.Scheduler'
SCHEDULER_PERSIST = True

最后,在redis的数据库中插入一个起始地址:lpush myspider:start_urls https://www.cnblogs.com/ 就可以运行了

posted @ 2020-12-23 10:18  NQ31  阅读(290)  评论(0编辑  收藏  举报