Python学习- 爬虫 - 队列/消息队列

请求管理业务

  • 请求去重管理:防止重复请求。(可以布隆过滤器)
  • 请求缓冲管理:临时存储请求
    • 常用数据结构:队列 Queue(常用), 列表,字典,元组
  • 请求调度管理:派遣并控制请求调度顺序,请求优先级管理
    • 请求调度优先级
      • 广度优先(FIFO队列)
      • 深度优先(LIFO队列)
      • 权重优先(优先级队列)

队列

临时队列

library/queue

  1. 内置队列模块 (Queue 常用于多线程,多进程从mutiprocess中引入)
  2. asyncio中的队列模块
  3. gevent中的队列模块
  4. tornado中的队列模块

为什么有这么多的队列? 因为每个队列一般都和异步模型,多线程,多进程通信用的,由于每个异步模型,在底层实现的原理不同,所以通信的时候处理方式有不同,所以会有多个队列模块。 但每个队列模块,api基本一样

持久化队列

  1. scrapy - queuelib 队列 (硬盘中 disk_queue),基于
    1. 有基于文件 sqlite 的 持久化
  2. pyspider - redis_queue 模块
    1. 基于 redis 的队列
      1. fifo队列 用 lists 列表,lpush,rpop
      2. lifo队列 用 lists 列表,lpush,lpop
      3. 优先级队列,用 Sorted Set 有序集合,zadd ,zrang ,zrem

redis锁及分布式锁

  • 因为在实现优先级队列时,zrange 取出队列中的值后,再zrem删除这个值(符合队列取出的逻辑),这中间会时间间隔,会出现多线程取值错误的情况
  • 所以需要锁机制
  1. 一般redis事务也可以保证操作的原子性,但是不像mysql事务,不能百分百保证
redis锁

py-crawler-demo

  • 一般用在单进程的多线程场景,如果是多进程的场景,则需要使用分布式锁
  • 说明:
  1. 使用了锁机制后,能确保在同一份数据只会被某一个线程获取到,而 不会被多个线程同时获取,从而保证了数据不会被处理多次的情况发 生 2.此处相当于实现了同一个线程内部zrange与zrem是一个原子性操作

注意

  1. 一般的内存中的锁只能解决单进程中多个线程间的资源共享问题;
  2. 如果是不同进程间甚至不同服务器上线程间资源共享问题,则需要考虑使用如redis分布式锁来实现
  3. redis虽有事务机制,但仍不足以保证前面理想的执行结果百分百出现
redis分布式锁
  • 说明:
  1. 获取锁:利用redis的setnx命令特征 如果key不存在则执行操作,返回值将是1,此时表明锁获取成功,即上锁;
  2. 如果key存在则不执行任何操作,返回值将是0,此时表明锁获取失败,因为已经被上锁了
  3. 释放锁:获取设置的值,判断是否是当前线程设置的值
  4. get命令获取对应key的值
  5. 判断值是否和预先设置的一样(thread id),保证不是其他线程解开的锁 .如果一致,就把该key删除,表示释放锁,此时其他线程便可以获取到锁
  • 注意:
  1. 同一把锁,注意lock_name一致
  2. 使用同一把锁的各个线程,必须维护好各自的 thread id,不能重复。否则可能出现,如a线程上的锁却被b线程解开了,这样的bug
  3. 为防止死锁问题(如a线程上了锁,但在解开锁前a线程挂了),应当给lock_name这个数据设置一定过期时间,具体时间,依实际情况定

锁代码示例

py-crawler-demo

from request_manager.utils.redis_tools import get_redis_queue_cls

Queue = get_redis_queue_cls('priority')
PRIORITY_REDIS_QUEUE_CONFIG = {
    "name":"pqueue",
    "use_lock":True,  # 锁
    "redis_lock_config":{
        "lock_name": "pqueue-lock"
    }
}

pqueue = Queue(**PRIORITY_REDIS_QUEUE_CONFIG)
pqueue.put((100, 'value100'))
print(pqueue.get())

  1. 因为 优先级队列 priority,是基于 redis 中的有序集合来实现的,要弹出某个值,是需要 zrange,和 zrem,两条命令中有时间差,所有加锁比较好

在优先级队列实现加锁

  1. 在取值方法中,先获取锁 self.lock.acquire_lock()
  2. 再进行一些列操作后,再释放锁 self.lock.release_lock()
  def get_nowait(self):
        """
        -1,-1默认取权重最大的
        0,,0 取权重最小的
        :return:
        """
        if self.use_lock is True:
            from .redis_lock import RedisLock
            if self.lock is None:
                self.lock = RedisLock(**self.redis_lock_config)

            if self.lock.acquire_lock():
                ret = self.redis.zrange(self.name, -1, -1)
                if not ret:
                    raise self.Empty
                self.redis.zrem(self.name, ret[0])
                self.lock.release_lock()
                return pickle.loads(ret[0])
        else:
            ret = self.redis.zrange(self.name, -1, -1)
            if not ret:
                raise self.Empty
            self.redis.zrem(self.name, ret[0])
            return pickle.loads(ret[0])

  1. 锁本身的实现
  • 基于 redis 中的用setnx设置某个值,(顺带加上过期时间,防止死锁,具体时间按照自己的业务逻辑来定合理的值)
  • 如果能设置,说明没锁;不能设置,说明已经上锁
  • 如果用户传了 block = True 参数,说明要阻塞,用一个 while 循环
  def acquire_lock(self, thread_id=None, expire=10 ,block = True):

    if thread_id is None:
        thread_id = self._get_thread_id()


    while block:
        # 如果 lock_name 存在,ret == 0 否则 ret==1
        ret = self.redis.setnx(self.lock_name, pickle.dumps(thread_id))
        if ret == 1:
            self.redis.expire(self.lock_name, expire)
            print('上锁成功')
            return True
        time.sleep(0.01)


 def release_lock(self, thread_id=None):
        if thread_id is None:
            thread_id = self._get_thread_id()

        ret = self.redis.get(self.lock_name)

        if ret is not None and pickle.loads(ret) == thread_id:
            self.redis.delete(self.lock_name)
            print('解锁成功')
            return True
        else:
            print('解锁失败')
            return False

# 测试
if __name__ == '__main__':
    redis_lock = RedisLock('redis_lock')

    if redis_lock.acquire_lock(expire=2):
        print('执行对应的操作')
        # redis_lock.release_lock()  # 加锁后不解锁试试

消息队列 - kafka

  1. kafka 服务,Broker 相当于这个服务的服务端,每个 Broker 就是一个kafka的实例,Producer 负责生产数据,Consumer消费数据,kafka接收到数据是持久化存储在 Disk 中的(可以海量数据)

  2. topic 相当于一条队列,分类 rowin90.github.io/images/py/k…

  3. 内部实现,数据均匀放在分区 part

    1. 比如一个1000条的数据,有3个part,有4个 Broker (相当于4个 kafka实例),副本集设置为2
    2. 如果Broker 只有1个,则不能有副本,副本必须与原数据在不同的Broer上 rowin90.github.io/images/py/k…

kafka 特点

  • Kafka启动:
    • 单节点单broker
    • 单节点多broker
  • Kafka使用时的显著特征
    • 分区之间是无序的,但分区内的消息是有序的
    • 对于topic的消费,消费者的数量应不多于该topic分区的数量,否则多余的消费者将必定无法接收到消息
    • 一个消费者可同时消费多个topic
    • Kafka保证每条消息在同—个Consumer Group里只会被某一个Consumer消费
---------------------------END---------------------------

题外话

当下这个大数据时代不掌握一门编程语言怎么跟的上脚本呢?当下最火的编程语言Python前景一片光明!如果你也想跟上时代提升自己那么请看一下.

在这里插入图片描述

感兴趣的小伙伴,赠送全套Python学习资料,包含面试题、简历资料等具体看下方。


👉CSDN大礼包🎁:全网最全《Python学习资料》免费赠送🆓!(安全链接,放心点击)

一、Python所有方向的学习路线

Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照下面的知识点去找对应的学习资源,保证自己学得较为全面。

img
img

二、Python必备开发工具

工具都帮大家整理好了,安装就可直接上手!img

三、最新Python学习笔记

当我学到一定基础,有自己的理解能力的时候,会去阅读一些前辈整理的书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。

img

四、Python视频合集

观看全面零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。

img

五、实战案例

纸上得来终觉浅,要学会跟着视频一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

img

六、面试宝典

在这里插入图片描述

在这里插入图片描述

简历模板在这里插入图片描述
👉 CSDN大礼包:gift::[全网最全《Python学习资料》免费赠送:free:!](https://blog.csdn.net/weixin_68789096/article/details/132275547?spm=1001.2014.3001.5502) (安全链接,放心点击)

若有侵权,请联系删除

Scrapy-Redis是一个基于Scrapy框架的分布式爬虫解决方案,它使用Redis作为分布式队列和去重集合,实现了多个爬虫节点共享一个Redis队列和去重集合,从而实现了高效的分布式爬取。 使用Scrapy-Redis,你可以很容易地将一个单机版的Scrapy爬虫转换成一个分布式爬虫。下面是简单的步骤: 1. 安装Redis和Scrapy-Redis 首先需要安装Redis,并且确保Redis服务正常运行。另外,需要安装Scrapy-Redis库,可以通过pip命令来进行安装: ``` pip install scrapy-redis ``` 2. 修改爬虫设置 在Scrapy爬虫的settings.py文件中,需要添加如下配置: ``` # 启用Redis调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 启用Redis去重过滤器 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 设置Redis为调度器和去重过滤器的数据存储位置 REDIS_URL = 'redis://localhost:6379' ``` 3. 修改爬虫代码 在Scrapy爬虫的代码中,需要将原来的start_urls修改为redis_key,例如: ``` class MySpider(RedisSpider): name = 'myspider' redis_key = 'myspider:start_urls' ``` 另外,需要将原来的parse方法改为如下形式: ``` def parse(self, response): # 爬虫代码 yield scrapy.Request(url, callback=self.parse_item) ``` 4. 运行爬虫 在启动爬虫之前,需要先往Redis队列中添加起始URL,例如: ``` redis-cli lpush myspider:start_urls http://www.example.com ``` 然后在命令行中启动分布式爬虫,例如: ``` scrapy crawl myspider ``` 这样就可以启动一个分布式的Scrapy爬虫了。在多个爬虫节点共享同一个Redis队列和去重集合的情况下,可以大大提高爬取效率和速度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值