zoukankan      html  css  js  c++  java
  • python采用 多进程/多线程/协程 写爬虫以及性能对比,牛逼的分分钟就将一个网站爬下来!

    首先我们来了解下python中的进程,线程以及协程!

    从计算机硬件角度:

    计算机的核心是CPU,承担了所有的计算任务。
    一个CPU,在一个时间切片里只能运行一个程序。

    从操作系统的角度:

    进程和线程,都是一种CPU的执行单元。

    进程:表示一个程序的上下文执行活动(打开、执行、保存...)

    线程:进程执行程序时候的最小调度单位(执行a,执行b...)

    一个程序至少有一个进程,一个进程至少有一个线程。

    并行 和 并发:


    并行:多个CPU核心,不同的程序就分配给不同的CPU来运行。可以让多个程序同时执行。

    cpu1 -------------
    cpu2 -------------
    cpu3 -------------
    cpu4 -------------

    并发:单个CPU核心,在一个时间切片里一次只能运行一个程序,如果需要运行多个程序,则串行执行。

    cpu1  ----  ----

    cpu1    ----  ----


    多进程/多线程:
    表示可以同时执行多个任务,进程和线程的调度是由操作系统自动完成。


    进程:每个进程都有自己独立的内存空间,不同进程之间的内存空间不共享。
    进程之间的通信有操作系统传递,导致通讯效率低,切换开销大。

    线程:一个进程可以有多个线程,所有线程共享进程的内存空间,通讯效率高,切换开销小。

    共享意味着竞争,导致数据不安全,为了保护内存空间的数据安全,引入"互斥锁"。

    一个线程在访问内存空间的时候,其他线程不允许访问,必须等待之前的线程访问结束,才能使用这个内存空间。

    互斥锁:一种安全有序的让多个线程访问内存空间的机制。

    Python的多线程:

    GIL 全局解释器锁:线程的执行权限,在Python的进程里只有一个GIL。

    一个线程需要执行任务,必须获取GIL。

    好处:直接杜绝了多个线程访问内存空间的安全问题。
    坏处:Python的多线程不是真正多线程,不能充分利用多核CPU的资源。

    但是,在I/O阻塞的时候,解释器会释放GIL。


    所以:

    多进程:密集CPU任务,需要充分使用多核CPU资源(服务器,大量的并行计算)的时候,用多进程。 multiprocessing
    缺陷:多个进程之间通信成本高,切换开销大。


    多线程:密集I/O任务(网络I/O,磁盘I/O,数据库I/O)使用多线程合适。
    threading.Thread、multiprocessing.dummy
    缺陷:同一个时间切片只能运行一个线程,不能做到高并行,但是可以做到高并发。


    协程:又称微线程,在单线程上执行多个任务,用函数切换,开销极小。不通过操作系统调度,没有进程、线程的切换开销。genvent,monkey.patchall

    多线程请求返回是无序的,那个线程有数据返回就处理那个线程,而协程返回的数据是有序的。

    缺陷:单线程执行,处理密集CPU和本地磁盘IO的时候,性能较低。处理网络I/O性能还是比较高.

    下面以这个网站为例,采用三种方式爬取。爬取前250名的电影。。

    https://movie.douban.com/top250?start=0

     通过分析网页发现第2页的url start=25,第3页的url start=50,第3页的start=75。因此可以得出这个网站每一页的数局是通过递增start这个参数获取的。

    一般不看第一页的数据,第一页的没有参考价值。

    这次我们主要爬取,电影名字跟评分。只是使用不同方式去对比下不同点,所以数据方面就不过多提取或者保存。只是简单的将其爬取下打印出来看看。

    第一:采用多进程 , multiprocessing 模块。 当然这个耗时更网络好坏有关。在全部要请求都正常的情况下耗时15s多。

    #!/usr/bin/env python2
    # -*- coding=utf-8 -*-
    
    from multiprocessing import Process, Queue
    
    import time
    from lxml import etree
    import requests
    
    
    class DouBanSpider(Process):
        def __init__(self, url, q):
            # 重写写父类的__init__方法
            super(DouBanSpider, self).__init__()
            self.url = url
            self.q = q
            self.headers = {
                'Host': 'movie.douban.com',
                'Referer': 'https://movie.douban.com/top250?start=225&filter=',
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36',
            }
    
        def run(self):
            self.parse_page()
    
        def send_request(self,url):
            '''
            用来发送请求的方法
            :return: 返回网页源码
            '''
            # 请求出错时,重复请求3次,
            i = 0
            while i <= 3:
                try:
                    print u"[INFO]请求url:"+url
                    return requests.get(url=url,headers=self.headers).content
                except Exception as e:
                    print u'[INFO] %s%s'% (e,url)
                    i += 1
    
        def parse_page(self):
            '''
            解析网站源码,并采用xpath提取 电影名称和平分放到队列中
            :return:
            '''
            response = self.send_request(self.url)
            html = etree.HTML(response)
            # 获取到一页的电影数据
            node_list = html.xpath("//div[@class='info']")
            for move in node_list:
                # 电影名称
                title = move.xpath('.//a/span/text()')[0]
                # 评分
                score = move.xpath('.//div[@class="bd"]//span[@class="rating_num"]/text()')[0]
               
                # 将每一部电影的名称跟评分加入到队列
                self.q.put(score + "	" + title)
    
    
    def main():
        # 创建一个队列用来保存进程获取到的数据
        q = Queue()
        base_url = 'https://movie.douban.com/top250?start='
        # 构造所有url
        url_list = [base_url+str(num) for num in range(0,225+1,25)]
    
        # 保存进程
        Process_list = []
        # 创建并启动进程
        for url in url_list:
            p = DouBanSpider(url,q)
            p.start()
            Process_list.append(p)
        
        # 让主进程等待子进程执行完成
        for i in Process_list:
            i.join()
    
        while not q.empty():
            print q.get()
    
    if __name__=="__main__":
        
        start = time.time()
        main()
        print '[info]耗时:%s'%(time.time()-start)
    Process多进程实现
    #!/usr/bin/env python2
    # -*- coding=utf-8 -*-
    
    from multiprocessing import Process, Queue
    
    import time
    from lxml import etree
    import requests
    
    
    class DouBanSpider(Process):
        def __init__(self, url, q):
            # 重写写父类的__init__方法
            super(DouBanSpider, self).__init__()
            self.url = url
            self.q = q
            self.headers = {
                'Host': 'movie.douban.com',
                'Referer': 'https://movie.douban.com/top250?start=225&filter=',
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36',
            }
    
        def run(self):
            self.parse_page()
    
        def send_request(self,url):
            '''
            用来发送请求的方法
            :return: 返回网页源码
            '''
            # 请求出错时,重复请求3次,
            i = 0
            while i <= 3:
                try:
                    print u"[INFO]请求url:"+url
                    return requests.get(url=url,headers=self.headers).content
                except Exception as e:
                    print u'[INFO] %s%s'% (e,url)
                    i += 1
    
        def parse_page(self):
            '''
            解析网站源码,并采用xpath提取 电影名称和平分放到队列中
            :return:
            '''
            response = self.send_request(self.url)
            html = etree.HTML(response)
            # 获取到一页的电影数据
            node_list = html.xpath("//div[@class='info']")
            for move in node_list:
                # 电影名称
                title = move.xpath('.//a/span/text()')[0]
                # 评分
                score = move.xpath('.//div[@class="bd"]//span[@class="rating_num"]/text()')[0]
               
                # 将每一部电影的名称跟评分加入到队列
                self.q.put(score + "	" + title)
    
    
    def main():
        # 创建一个队列用来保存进程获取到的数据
        q = Queue()
        base_url = 'https://movie.douban.com/top250?start='
        # 构造所有url
        url_list = [base_url+str(num) for num in range(0,225+1,25)]
    
        # 保存进程
        Process_list = []
        # 创建并启动进程
        for url in url_list:
            p = DouBanSpider(url,q)
            p.start()
            Process_list.append(p)
        
        # 让主进程等待子进程执行完成
        for i in Process_list:
            i.join()
    
        while not q.empty():
            print q.get()
    
    if __name__=="__main__":
        
        start = time.time()
        main()
        print '[info]耗时:%s'%(time.time()-start)
    

      

      采用多线程时,耗时10.4s

    #!/usr/bin/env python2
    # -*- coding=utf-8 -*-
    
    from threading import Thread
    from Queue import Queue
    import time
    from lxml import etree
    import requests
    
    
    class DouBanSpider(Thread):
        def __init__(self, url, q):
            # 重写写父类的__init__方法
            super(DouBanSpider, self).__init__()
            self.url = url
            self.q = q
            self.headers = {
                'Cookie': 'll="118282"; bid=ctyiEarSLfw; ps=y; __yadk_uid=0Sr85yZ9d4bEeLKhv4w3695OFOPoedzC; dbcl2="155150959:OEu4dds1G1o"; as="https://sec.douban.com/b?r=https%3A%2F%2Fbook.douban.com%2F"; ck=fTrQ; _pk_id.100001.4cf6=c86baf05e448fb8d.1506160776.3.1507290432.1507283501.; _pk_ses.100001.4cf6=*; __utma=30149280.1633528206.1506160772.1507283346.1507290433.3; __utmb=30149280.0.10.1507290433; __utmc=30149280; __utmz=30149280.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=223695111.1475767059.1506160772.1507283346.1507290433.3; __utmb=223695111.0.10.1507290433; __utmc=223695111; __utmz=223695111.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); push_noty_num=0; push_doumail_num=0',
                'Host': 'movie.douban.com',
                'Referer': 'https://movie.douban.com/top250?start=225&filter=',
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36',
            }
    
        def run(self):
            self.parse_page()
    
        def send_request(self,url):
            '''
            用来发送请求的方法
            :return: 返回网页源码
            '''
            # 请求出错时,重复请求3次,
            i = 0
            while i <= 3:
                try:
                    print u"[INFO]请求url:"+url
                    html = requests.get(url=url,headers=self.headers).content
                except Exception as e:
                    print u'[INFO] %s%s'% (e,url)
                    i += 1
                else:
                    return html
    
        def parse_page(self):
            '''
            解析网站源码,并采用xpath提取 电影名称和平分放到队列中
            :return:
            '''
            response = self.send_request(self.url)
            html = etree.HTML(response)
            # 获取到一页的电影数据
            node_list = html.xpath("//div[@class='info']")
            for move in node_list:
                # 电影名称
                title = move.xpath('.//a/span/text()')[0]
                # 评分
                score = move.xpath('.//div[@class="bd"]//span[@class="rating_num"]/text()')[0]
    
                # 将每一部电影的名称跟评分加入到队列
                self.q.put(score + "	" + title)
    
    
    def main():
        # 创建一个队列用来保存进程获取到的数据
        q = Queue()
        base_url = 'https://movie.douban.com/top250?start='
        # 构造所有url
        url_list = [base_url+str(num) for num in range(0,225+1,25)]
    
        # 保存线程
        Thread_list = []
        # 创建并启动线程
        for url in url_list:
            p = DouBanSpider(url,q)
            p.start()
            Thread_list.append(p)
    
        # 让主线程等待子线程执行完成
        for i in Thread_list:
            i.join()
    
        while not q.empty():
            print q.get()
    
    if __name__=="__main__":
    
        start = time.time()
        main()
        print '[info]耗时:%s'%(time.time()-start)
    thread
    #!/usr/bin/env python2
    # -*- coding=utf-8 -*-
    
    from threading import Thread
    from Queue import Queue
    import time
    from lxml import etree
    import requests
    
    
    class DouBanSpider(Thread):
        def __init__(self, url, q):
            # 重写写父类的__init__方法
            super(DouBanSpider, self).__init__()
            self.url = url
            self.q = q
            self.headers = {
                'Cookie': 'll="118282"; bid=ctyiEarSLfw; ps=y; __yadk_uid=0Sr85yZ9d4bEeLKhv4w3695OFOPoedzC; dbcl2="155150959:OEu4dds1G1o"; as="https://sec.douban.com/b?r=https%3A%2F%2Fbook.douban.com%2F"; ck=fTrQ; _pk_id.100001.4cf6=c86baf05e448fb8d.1506160776.3.1507290432.1507283501.; _pk_ses.100001.4cf6=*; __utma=30149280.1633528206.1506160772.1507283346.1507290433.3; __utmb=30149280.0.10.1507290433; __utmc=30149280; __utmz=30149280.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=223695111.1475767059.1506160772.1507283346.1507290433.3; __utmb=223695111.0.10.1507290433; __utmc=223695111; __utmz=223695111.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); push_noty_num=0; push_doumail_num=0',
                'Host': 'movie.douban.com',
                'Referer': 'https://movie.douban.com/top250?start=225&filter=',
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36',
            }
    
        def run(self):
            self.parse_page()
    
        def send_request(self,url):
            '''
            用来发送请求的方法
            :return: 返回网页源码
            '''
            # 请求出错时,重复请求3次,
            i = 0
            while i <= 3:
                try:
                    print u"[INFO]请求url:"+url
                    html = requests.get(url=url,headers=self.headers).content
                except Exception as e:
                    print u'[INFO] %s%s'% (e,url)
                    i += 1
                else:
                    return html
    
        def parse_page(self):
            '''
            解析网站源码,并采用xpath提取 电影名称和平分放到队列中
            :return:
            '''
            response = self.send_request(self.url)
            html = etree.HTML(response)
            # 获取到一页的电影数据
            node_list = html.xpath("//div[@class='info']")
            for move in node_list:
                # 电影名称
                title = move.xpath('.//a/span/text()')[0]
                # 评分
                score = move.xpath('.//div[@class="bd"]//span[@class="rating_num"]/text()')[0]
    
                # 将每一部电影的名称跟评分加入到队列
                self.q.put(score + "	" + title)
    
    
    def main():
        # 创建一个队列用来保存进程获取到的数据
        q = Queue()
        base_url = 'https://movie.douban.com/top250?start='
        # 构造所有url
        url_list = [base_url+str(num) for num in range(0,225+1,25)]
    
        # 保存线程
        Thread_list = []
        # 创建并启动线程
        for url in url_list:
            p = DouBanSpider(url,q)
            p.start()
            Thread_list.append(p)
    
        # 让主线程等待子线程执行完成
        for i in Thread_list:
            i.join()
    
        while not q.empty():
            print q.get()
    
    if __name__=="__main__":
    
        start = time.time()
        main()
        print '[info]耗时:%s'%(time.time()-start)
    

      

     

    采用协程爬取,耗时15S,

    #!/usr/bin/env python2
    # -*- coding=utf-8 -*-
    
    from Queue import Queue
    import time
    from lxml import etree
    import requests
    import gevent
    
    # 打上猴子补丁
    from gevent import monkey
    monkey.patch_all()
    
    class DouBanSpider(object):
        def __init__(self):
            # 创建一个队列用来保存进程获取到的数据
            self.q = Queue()
            self.headers = {
                'Cookie': 'll="118282"; bid=ctyiEarSLfw; ps=y; __yadk_uid=0Sr85yZ9d4bEeLKhv4w3695OFOPoedzC; dbcl2="155150959:OEu4dds1G1o"; as="https://sec.douban.com/b?r=https%3A%2F%2Fbook.douban.com%2F"; ck=fTrQ; _pk_id.100001.4cf6=c86baf05e448fb8d.1506160776.3.1507290432.1507283501.; _pk_ses.100001.4cf6=*; __utma=30149280.1633528206.1506160772.1507283346.1507290433.3; __utmb=30149280.0.10.1507290433; __utmc=30149280; __utmz=30149280.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=223695111.1475767059.1506160772.1507283346.1507290433.3; __utmb=223695111.0.10.1507290433; __utmc=223695111; __utmz=223695111.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); push_noty_num=0; push_doumail_num=0',
                'Host': 'movie.douban.com',
                'Referer': 'https://movie.douban.com/top250?start=225&filter=',
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36',
            }
    
        def run(self,url):
            self.parse_page(url)
    
        def send_request(self,url):
            '''
            用来发送请求的方法
            :return: 返回网页源码
            '''
            # 请求出错时,重复请求3次,
            i = 0
            while i <= 3:
                try:
                    print u"[INFO]请求url:"+url
                    html = requests.get(url=url,headers=self.headers).content
                except Exception as e:
                    print u'[INFO] %s%s'% (e,url)
                    i += 1
                else:
                    return html
    
        def parse_page(self,url):
            '''
            解析网站源码,并采用xpath提取 电影名称和平分放到队列中
            :return:
            '''
            response = self.send_request(url)
            html = etree.HTML(response)
            # 获取到一页的电影数据
            node_list = html.xpath("//div[@class='info']")
            for move in node_list:
                # 电影名称
                title = move.xpath('.//a/span/text()')[0]
                # 评分
                score = move.xpath('.//div[@class="bd"]//span[@class="rating_num"]/text()')[0]
    
                # 将每一部电影的名称跟评分加入到队列
                self.q.put(score + "	" + title)
    
    
        def main(self):
    
    
            base_url = 'https://movie.douban.com/top250?start='
            # 构造所有url
            url_list = [base_url+str(num) for num in range(0,225+1,25)]
            # 创建协程并执行
            job_list = [gevent.spawn(self.run,url) for url in url_list]
            # 让线程等待所有任务完成,再继续执行。
            gevent.joinall(job_list)
    
            while not self.q.empty():
                print self.q.get()
    
    if __name__=="__main__":
        start = time.time()
        douban = DouBanSpider()
        douban.main()
        print '[info]耗时:%s'%(time.time()-start)
    gevent

     

    用了多进程,多线程,协程,实现的代码都一样,没有测试出明显的那个好!都不分上下,可能跟网络,或者服务器配置有关。

    但理论上来说线程,协程在I/O密集的操作性能是要高于进程的。

    也可能是我的方法有问题,还望大神们指教!

     如果你支持我,就扫扫我的红包,你领我几毛,我领几毛,也算是对我的支持。

  • 相关阅读:
    P2522 [HAOI2011]Problem b(容斥)
    P3455 [POI2007]ZAP-Queries
    P2519 [HAOI2011]problem a(线段树优化dp+思维)
    P2516 [HAOI2010]最长公共子序列 (lcs+容斥)
    [HAOI2010]软件安装(缩点+树形dp)
    P2508 [HAOI2008]圆上的整点(神仙题)
    [SDOI2011]消防(树的直径+二分||单调队列)
    QLabel设置字体颜色
    Qt绘制不规则串口
    C++继承关系
  • 原文地址:https://www.cnblogs.com/huangguifeng/p/7632799.html
Copyright © 2011-2022 走看看