在Python网络爬虫程序中使用生产者消费者模式爬取数据

一、生产者与消费者模式

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

生产者—>缓冲区—>消费者

二、队列Queue与进程间通信

关于队列Queue的一般描述,请参考我的另一篇博文:
《Python并发编程之Queue队列》

关于进程间通信的相关知识(生产者消费者通过JoinableQueue实现进程间通信),可以参考我的另一篇博文:
Python并发编程之进程间通信

可以通过multiprocessing模块中的JoinableQueue类来创建一个共享进程队列。

JoinableQueue([maxsize])

这就像是一个Queue对象,但队列允许项目的使用者通知生产者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
JoinableQueue的实例p除了与Queue对象相同的方法外,还具有以下方法:

  • q.task_done()
    消费者使用此方法发出信号,表示q.get()返回的项目已经被处理完毕。如果调用此方法的次数大于队列中删除的项目数量,将引发ValueError异常
  • q.join()
    生产者使用此方法进行阻塞,直到队列中的所有项目都被处理。阻塞将持续到位队列中的每个项目均调用q.task_done()方法为止。

三、在Python网络爬虫程序中使用队列进行进程间通信

在python网络爬虫程序中,有时我们需要爬取很多page页,然后每个page页上又包含有很多篇文章,每篇文章里又有众多的资源文件需要下载。那么我们可以通过生产者消费者的模式来实现内容的爬取。

生产者负责解析每一个page页,并且将每个page页的所有文章解析完毕,提取出里面需要下载的内容,放到队列中

消费者从队列中,获取需要下载的内容,进行下载,直至队列里所有的元素全部被处理完毕。

以下代码做了生产者消费者模式的示范,细节部分请参考代码中的注释:
main.py

import multiprocessing
import os
from home_page_parser import HomePageParser
from page_content_handler import PageContentConsumer, PageContentProducer

def main(dir):
    # 先解析出每一页的url, 生成一个page_list列表
    home_page_parser = HomePageParser()
    home_page_parser.parse_home_page()
    page_list = home_page_parser.get_page_list()

    # 创建一个队列, 用于生产者和消费者进行进程间同步
    queue = multiprocessing.JoinableQueue()
    
    # 创建多个消费者, 消费者的个数取决于cpu的数目
    consumer_num = os.cpu_count()
    consumers = []

    for i in range(0, consumer_num):
        consumers.append(PageContentConsumer(dir, queue))
    
    print(f'total {consumer_num} consumers')
    
    # 启动消费者进程
    for i in range(0, consumer_num):
        consumers[i].start()
    
    # 启动生产者进程, 并等待它执行完毕
    producer = PageContentProducer(page_list, queue)
    producer.start()
    producer.join()
    
    # 在队列上放置标志,这里我们使用None作为标志,发出完成信号
    # 注意, 总共有多少个消费者,就需要放置多少个标志
    for i in range(0, consumer_num):
        queue.put(None)
        
    # 等待所有消费者进程关闭
    for i in range(0, consumer_num):
        consumers[i].join()
    
if __name__ == '__main__':
    multiprocessing.freeze_support()
    
    dir = 'd:test/consumer2'
    main(dir)

生产者和消费者进程的示意代码:
page_content_handler.py

class PageContentProducer(multiprocessing.Process):
    def __init__(self, page_list:list, output_queue:multiprocessing.JoinableQueue):
        multiprocessing.Process.__init__(self)
        self.daemon = True
        self.page_list = page_list
        self.content_list = []
        self.output_queue = output_queue
    
    def run(self):
        '''
        向队列中加入每一篇文章
        '''
        self.visit_all_page_to_get_content()
        
        for content in self.content_list:
            print(f"已加入: {content['title']}")
            self.output_queue.put(content)
        
    def visit_all_page_to_get_content(self):
        '''
        使用线程池处理所有的page, 并从每一页上提取所有的文章content
        '''
        # 在 3.8 版更改: max_workers 的默认值已改为 min(32, os.cpu_count() + 4)。这个默认值会
        # 保留至少 5 个工作线程用于 I/O 密集型任务。对于那些释放了 GIL 的 CPU 密集型任务,它最多会
        # 使用 32 个 CPU 核心。这样能够避免在多核机器上不知不觉地使用大量资源。
        # 现在 ThreadPoolExecutor 在启动 max_workers 个工作线程之前也会重用空闲的工作线程。
        
        # We can use a with statement to ensure threads are cleaned up promptly
        with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
            # 向线程池提交任务
            future_to_page = {executor.submit(self.get_page_content, page_url) : page_url for page_url in self.page_list}
            for future in concurrent.futures.as_completed(future_to_page):
                page = future_to_page[future]
                try:
                    # 获取任务执行结果
                    result_list = future.result()
                    self.content_list += result_list
                except Exception as e:
                    print(f'{page} generated an exception: {e}')
        print(f'共提取到{len(self.content_list)}条content记录')
        
    def get_page_content(self, page_url) -> list:
        '''
        线程工作函数, 访问page_url, 并提取该页面上的所有文章, 以列表形式返回
        '''
        content_list = []
        try:
            res = requests.get(url=page_url)
            if 200 == res.status_code:
                page_html = res.content.decode('utf-8')
                soup = bs(page_html, 'lxml')
                items = soup.find_all('li', onclick=re.compile('.*content_[0-9]*.*'))
                print(f'从page: {page_url} 上提取到了[{len(items)}]个content')

                for item in items:
                    content = {}
                    # 提取标题
                    item_title = item.find('a', href='#')
                    content['title'] = item_title.text
                    # 提取图片数目
                    item_num = item.find('span')
                    content['num'] = item_num.text
                    # 提取url, 格式为location.href='content_48388.html';
                    href = item['onclick']
                    item_url = href.split("'")[1]
                    content['url'] = 'https://xxxx.xyz/' + item_url
                    content_list.append(content)
        except Exception as e:
            print(f'从page: {page_url} 上添加content失败')
            print(repr(e))
        return content_list


class PageContentConsumer(multiprocessing.Process):
    def __init__(self, dir, input_queue:multiprocessing.JoinableQueue):
        multiprocessing.Process.__init__(self)
        self.daemon = True
        self.input_queue = input_queue
        self.dir = dir
        
    def run(self):
        while True:
            try:
                content = self.input_queue.get()
                if content is None:
                    # 如果收到结束标志, 就退出当前任务
                    break
                self.content_worker_func(self.dir, content)
                print(f"已处理: {content['title']}")
                # 发出信号通知任务完成
                self.input_queue.task_done()
            except Exception as e:
                print(repr(e))
                
    def content_worker_func(self, dir, content):
        title = content['title']
        num = content['num']
        content_url = content['url']
        count = 0
        img_url_list = []
        pid = os.getpid()
        
        try:
            folder_name = title + '_' + num
            path = os.path.join(dir, folder_name)
            if os.path.isdir(path):
                pass
            else:
                os.makedirs(path)

            content_parser = ContentPageParser(content_url)
            content_parser.visit_content_page_with_firefox()
            img_url_list = content_parser.get_img_src()
        except Exception as e:
            print(repr(e))

        pic_download_threads = []
        for img_url in img_url_list:
            count += 1
            file_name = 'img_' + str(count)
            thread_name = str(pid) + ':' + str(count)
            pic_download_threads.append(PicDownloader(thread_name, img_url, file_name, path))

        for working_thread in pic_download_threads:
            working_thread.start()
        
        for working_thread in pic_download_threads:
            working_thread.join(3)
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
【为什么学爬虫?】        1、爬虫入手容易,但是深入较难,如何写出高效率的爬虫,如何写出灵活性高可扩展的爬虫都是一项技术活。另外在爬虫过程,经常容易遇到被反爬虫,比如字体反爬、IP识别、验证码等,如何层层攻克难点拿到想要的数据,这门课程,你都能学到!        2、如果是作为一个其他行业的开发者,比如app开发,web开发,学习爬虫能让你加强对技术的认知,能够开发出更加安全的软件和网站 【课程设计】 一个完整的爬虫程序,无论大小,总体来说可以分成三个步骤,分别是:网络请求:模拟浏览器的行为从网上抓取数据数据解析:将请求下来的数据进行过滤,提取我们想要的数据数据存储:将提取到的数据存储到硬盘或者内存。比如用mysql数据库或者redis等。那么本课程也是按照这几个步骤循序渐进的进行讲解,带领学生完整的掌握每个步骤的技术。另外,因为爬虫的多样性,在爬取的过程可能会发生被反爬、效率低下等。因此我们又增加了两个章节用来提高爬虫程序的灵活性,分别是:爬虫进阶:包括IP代理,多线程爬虫,图形验证码识别、JS加密解密、动态网页爬虫、字体反爬识别等。Scrapy和分布式爬虫:Scrapy框架、Scrapy-redis组件、分布式爬虫等。通过爬虫进阶的知识点我们能应付大量的反爬网站,而Scrapy框架作为一个专业的爬虫框架,使用他可以快速提高我们编写爬虫程序的效率和速度。另外如果一台机器不能满足你的需求,我们可以用分布式爬虫让多台机器帮助你快速爬取数据。 从基础爬虫到商业化应用爬虫,本套课程满足您的所有需求!【课程服务】 专属付费社群+定期答疑

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

smart_cat

你的鼓励将是我写作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值