多进程/多线程/协程爬虫的性能比较

进程、线程、协程

首先我们先来了解一下python中的进程、线程和协程。

进程和线程

从计算机硬件角度:

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

从操作系统的角度:
进程和线程,都是一种CPU的执行单元。

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

线程: 进程执行程序时的最小调度单位(执行a,执行b…)
一个程序至少有一个进程,一个进程至少有一个线程。

打个比方,我们打开QQ,这就创建了一个进程,而在QQ里打开多个聊天窗口和别人聊天,这就创建了多个线程。

并行和并发

并行: 多个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性能还是比较高.

对于多进程、多线程、协程的通俗解释

进程、线程、协程对比

请仔细理解如下的通俗描述

有一个老板想要开个工厂进行生产某件商品(例如剪子)

他需要花一些财力物力制作一条生产线,这个生产线上有很多的器件以及材料这些所有的 为了能够生产剪子而准备的资源称之为:进程

只有生产线是不能够进行生产的,所以老板的找个工人来进行生产,这个工人能够利用这些材料最终一步步的将剪子做出来,这个来做事情的工人称之为:线程

这个老板为了提高生产率,想到3种办法:

1、在这条生产线上多招些工人,一起来做剪子,这样效率是成倍増长,即单进程 多线程方式(一条生产线多个工人)

2、老板发现这条生产线上的工人不是越多越好,因为一条生产线的资源以及材料毕竟有限,所以老板又花了些财力物力购置了另外一条生产线,然后再招些工人这样效率又再一步提高了,即多进程 多线程方式

3、老板发现,现在已经有了很多条生产线,并且每条生产线上已经有很多工人了(即程序是多进程的,每个进程中又有多个线程),为了再次提高效率,老板想了个损招,规定:如果某个员工在上班时临时没事或者再等待某些条件(比如等待另一个工人生产完谋道工序 之后他才能再次工作) ,那么这个员工就利用这个时间去做其它的事情,那么也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,其实这就是:协程方式

简单总结

l 进程是资源分配的单位

l 线程是操作系统调度的单位

l 进程切换需要的资源很最大,效率很低

l 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)

l 协程切换任务资源很小,效率高

l 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发

实战案例:爬取豆瓣电影TOP250

单线程

import requests
from lxml import etree
import time
start=time.time()
base_url='https://movie.douban.com/top250?start={}'
headers={

    'Host': 'movie.douban.com',
    'Referer': 'https://movie.douban.com/top250?start=0',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36'
}
for page in range(0,11):
    url=base_url.format(page*25)
    response=requests.get(url,headers=headers)
    html=etree.HTML(response.text)
    info_list=html.xpath('//*[@id="content"]/div/div[1]/ol/li')
    for info in info_list:
        name=info.xpath('div/div[2]/div[1]/a/span[1]/text()')[0]
        score=info.xpath('div/div[2]/div[2]/div/span[2]/text()')[0]
        print(score,name)
end=time.time()
print('耗时%s秒'%(end-start))

在这里插入图片描述

多线程

from multiprocessing import Queue
from threading import Thread
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 = {
            '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 + "\t" + title)
            print(self.q.get())


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))

在这里插入图片描述

多进程

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 + "\t" + title)
            print(self.q.get())


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))

在这里插入图片描述

协程

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 + "\t" + 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)

参考链接:
1.多线程、多进程简介+代码示例
2.【多进程、多线程、协程+异步】对比测试
3.python采用 多进程/多线程/协程 写爬虫以及性能对比,牛逼的分分钟就将一个网站爬下来!..
4.进程、线程、协程对比通俗解释
5.单线程、多线程和协程的爬虫性能对比(含彩蛋)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值