zoukankan      html  css  js  c++  java
  • 分布式爬虫

    介绍

    原来scrapy的Scheduler维护的是本机的任务队列(存放Request对象及其回调函数等信息)+本机的去重队列(存放访问过的url地址)

    ![](https://raw.githubusercontent.com/cwyfengyiyuan/images/master/1598067296_20200806171556166_15454.png =719x)

    所以实现分布式爬取的关键就是,找一台专门的主机上运行一个共享的队列比如Redis,
    然后重写Scrapy的Scheduler,让新的Scheduler到共享队列存取Request,并且去除重复的Request请求,所以总结下来,实现分布式的关键就是三点:

    #1、共享队列
    #2、重写Scheduler,让其无论是去重还是任务都去访问共享队列
    #3、为Scheduler定制去重规则(利用redis的集合类型)
    

    scrapy-redis安装使用

    #安装:
    pip3 install scrapy-redis
    
    #源码:
    D:python3.6Libsite-packagesscrapy_redis
    

    scrapy-redis的去重功能

    #一、源码:D:python3.6Libsite-packagesscrapy_redisdupefilter.py
    
    #二、配置scrapy使用redis提供的共享去重队列
    
    #2.1 在settings.py中配置链接Redis
    REDIS_HOST = 'localhost'                            # 主机名
    REDIS_PORT = 6379                                   # 端口
    REDIS_URL = 'redis://user:pass@hostname:9001'       # 连接URL(优先于以上配置)
    REDIS_PARAMS  = {}                                  # Redis连接参数
    REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient' # 指定连接Redis的Python模块
    REDIS_ENCODING = "utf-8"                            # redis编码类型  
    # 默认配置:D:python3.6Libsite-packagesscrapy_redisdefaults.py
    
    
    #2.2 让scrapy使用共享的去重队列
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
    #使用scrapy-redis提供的去重功能,查看源码会发现是基于Redis的集合实现的
    
    
    #2.3、需要指定Redis中集合的key名,key=存放不重复Request字符串的集合
    DUPEFILTER_KEY = 'dupefilter:%(timestamp)s'
    #源码:dupefilter.py内一行代码key = defaults.DUPEFILTER_KEY % {'timestamp': int(time.time())}
    
    
    #2.4、去重规则源码分析dupefilter.py
    def request_seen(self, request):
        """Returns True if request was already seen.
    
        Parameters
        ----------
        request : scrapy.http.Request
    
        Returns
        -------
        bool
    
        """
        fp = self.request_fingerprint(request) 
        # This returns the number of values added, zero if already exists.
        added = self.server.sadd(self.key, fp)
        return added == 0
    
    
    #2.5、将request请求转成一串字符后再存入集合
    
    from scrapy.http import Request
    from scrapy.utils.request import request_fingerprint
    
    req = Request(url='http://www.baidu.com')
    result=request_fingerprint(req)
    print(result) #75d6587d87b3f4f3aa574b33dbd69ceeb9eafe7b
    
    
    #2.6、注意:
        - URL参数位置不同时,计算结果一致;
        - 默认请求头不在计算范围,include_headers可以设置指定请求头
        - 示范:
        from scrapy.utils import request
        from scrapy.http import Request
         
        req = Request(url='http://www.baidu.com?name=8&id=1',callback=lambda x:print(x),cookies={'k1':'vvvvv'})
        result1 = request.request_fingerprint(req,include_headers=['cookies',])
         
        print(result)
         
        req = Request(url='http://www.baidu.com?id=1&name=8',callback=lambda x:print(x),cookies={'k1':666})
         
        result2 = request.request_fingerprint(req,include_headers=['cookies',])
         
        print(result1 == result2) #True
    

    使用scrapy-redis的去重+调度实现分布式爬取

    #1、源码:D:python3.6Libsite-packagesscrapy_redisscheduler.py
    
    
    #2、settings.py配置
    
    # Enables scheduling storing requests queue in redis.
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"       
    
    # 调度器将不重复的任务用pickle序列化后放入共享任务队列,默认使用优先级队列(默认),其他:PriorityQueue(有序集合),FifoQueue(列表)、LifoQueue(列表)               
    SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'          
    
    # 对保存到redis中的request对象进行序列化,默认使用pickle
    SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat"   
    
    # 调度器中请求任务序列化后存放在redis中的key               
    SCHEDULER_QUEUE_KEY = '%(spider)s:requests'    
    
    # 是否在关闭时候保留原来的调度器和去重记录,True=保留,False=清空                     
    SCHEDULER_PERSIST = True       
    
    # 是否在开始之前清空 调度器和去重记录,True=清空,False=不清空                                     
    SCHEDULER_FLUSH_ON_START = False    
    
    # 去调度器中获取数据时,如果为空,最多等待时间(最后没数据,未获取到)。如果没有则立刻返回会造成空循环次数过多,cpu占用率飙升                                
    SCHEDULER_IDLE_BEFORE_CLOSE = 10           
    
    # 去重规则,在redis中保存时对应的key                         
    SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter'      
    
    # 去重规则对应处理的类,将任务request_fingerprint(request)得到的字符串放入去重队列            
    SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
    

    持久化

    #从目标站点获取并解析出数据后保存成item对象,会由引擎交给pipeline进行持久化/保存到数据库,scrapy-redis提供了一个pipeline组件,可以帮我们把item存到redis中
         
    #1、将item持久化到redis时,指定key和序列化函数 
    REDIS_ITEMS_KEY = '%(spider)s:items'
    REDIS_ITEMS_SERIALIZER = 'json.dumps'
     
    #2、使用列表保存item数据
    

    从Redis中获取起始URL

    scrapy程序爬取目标站点,一旦爬取完毕后就结束了,如果目标站点更新内容了,我们想重新爬取,那么只能再重新启动scrapy,非常麻烦
    scrapy-redis提供了一种供,让scrapy从redis中获取起始url,如果没有scrapy则过一段时间再来取而不会关闭
    这样我们就只需要写一个简单的脚本程序,定期往redis队列里放入一个起始url。
    
    #具体配置如下
    
    #1、编写爬虫时,起始URL从redis的Key中获取
    REDIS_START_URLS_KEY = '%(name)s:start_urls'
        
    #2、获取起始URL时,去集合中获取还是去列表中获取?True,集合;False,列表
    REDIS_START_URLS_AS_SET = False    # 获取起始URL时,如果为True,则使用self.server.spop;如果为False,则使用self.server.lpop
    

    分布爬取cnblog全栈文章

    # cnblog_redis.py
    import scrapy
    from scrapy_redis.spiders import RedisSpider
    from cnblogs.items import CnblogsItem
    from scrapy import Request
    
    class CnblogSpider(RedisSpider):
        name = 'cnblog_redis'
        allowed_domains = ['www.cnblogs.com']
        # start_urls = ['https:/www.cnblogs.com/']
        redis_key = 'myspider:start_urls' # 打开redis,手动在redis中以myspider:start_urls为键插入起始链接(lpush myspider:start_urls https://ip)
    
        def parse(self, response):
            # print(response.text)
            div_list=response.css('article.post-item')
            for div in div_list:
                item=CnblogsItem()
                title=div.xpath('.//div[1]/a/text()').extract_first()
                item['title']=title
                url=div.xpath('.//div[1]/a/@href').extract_first()
                item['url'] = url
                desc=div.xpath('.//div[1]/p/text()').extract_first().strip()
                item['desc'] = desc
                # 要继续爬取详情
                # callback如果不写,默认回调到parse方法
                # 如果写了,响应回来的对象就会调到自己写的解析方法中
                yield Request(url,callback=self.parser_detail,meta={'item':item})
    
            # 解析出下一页的地址
            next='https://www.cnblogs.com'+response.css('#paging_block>div a:last-child::attr(href)').extract_first()
            print(next)
            yield Request(next)
    
        def parser_detail(self,response):
    
            content=response.css('#cnblogs_post_body').extract_first()
            print(str(content))
            # item哪里来
            item=response.meta.get('item')
            item['content']=content
            print(item)
            yield item
    
    
    # items.py
    import scrapy
    
    class CnblogsItem(scrapy.Item):
        title = scrapy.Field()
        url = scrapy.Field()
        desc = scrapy.Field()
        content = scrapy.Field()
    
    
    # pipelines.py
    import pymysql
    class CnblogsMysqlPipeline(object):
        def open_spider(self,spider):
            #爬虫对象
            print('-------',spider.name)
            self.conn=pymysql.connect( host='127.0.0.1', user='root', password="123",database='cnblogs', 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()
    
    
    # settings.py
    # 持久化的可以配置,也可以不配置
    ITEM_PIPELINES = {
       ...
       'scrapy_redis.pipelines.RedisPipeline': 299
    }
    
    # 使用scrapy-redis的去重
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
    
    # 使用scrapy-redis的Scheduler
    # 分布式爬虫的配置
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"
    
    
    '''
    如果不配置redis默认为本地redis
    '''
    # 在settings.py中配置Redis
    REDIS_HOST = 'localhost'                            # 主机名
    REDIS_PORT = 6379                                   # 端口
    REDIS_URL = 'redis://user:pass@hostname:9001'       # 连接URL(优先于以上配置)
    REDIS_PARAMS  = {}                                  # Redis连接参数
    REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient' # 指定连接Redis的Python模块
    REDIS_ENCODING = "utf-8"                            # redis编码类型  
    
  • 相关阅读:
    XML(学习笔记)
    css样式学习笔记
    Request(对象)
    sql一些错误修改的总结
    转载(如何学习C#)
    sql server(学习笔记2 W3Cschool)
    sql sqrver(学习笔记1 W3Cschool)
    关于 flutter开发碰到的各种问题,有的已经解决有的一直没解决或者用其他方法替代
    关于 Flutter IOS build It appears that your application still contains the default signing identifier.
    关于 flutter本地化问题 The getter 'pasteButtonLabel' was called on null
  • 原文地址:https://www.cnblogs.com/chenwenyin/p/13544896.html
Copyright © 2011-2022 走看看