Python爬虫中多线程、多进程、多协程的介绍和对比

前面咱们也写了好几篇爬虫的文章,都是些数据量不是很多的例子。那如果遇到数据量比较大的时候怎么办提高速度和效率呢。今天的这篇会给你答案。

一、线程、进程、协程的简介

1.同步、异步、并行、并发

同步:各个任务不是独自运行的,任务之间有一定的运行顺序
异步:各个任务是独立运行的,一个任务的运行不收另一个任务的影响
并行:同一时刻发生若干事情的情况
并发:同一时间段发生若干事情。

这里可能并行和并发不是很好记忆,我来举个例子把。小明正在写作业,小红正在看电视。两件事情同时进行,这叫并行。如果小明看一眼电视,看一眼作业,这叫做并发,并不能同时做这两件事,只不过这两件事切换比较快,看起来像是同时在发生。

在单核CPU时,所有任务都是以并发的形式来运行的,
在多核CPU情况下,才会有真正的并行的形式运行任务。

2.线程

  • 操作系统进行运算的最小单位
  • 线程是独立调度和分派的基本单位
  • 同一个进程中的所有线程可以共享该进程的资源
  • 同一进程中的所有线程均可并发执行
  • 由于Python中GIL(全局解释器锁)的存在,线程之间只能以并发执行存在。

3.进程

  • 系统进行资源分配和调度的基本单位
  • 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)
  • 程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程
  • 运行中的进程可能存在以下三种情况:就绪、运行、阻塞

4.协程

  • 一种用户态的轻量级线程。
  • 协程方便切换控制流
  • 具有很好的高扩展性和高并发行
  • 本质是单线程

二、多线程、多进程、多协程者的区别

图1 多线程的运行方式多线程的运行方式
图2 多进程的运行方式
在这里插入图片描述
图3 多协程的运行方式
在这里插入图片描述

  • 每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
  • 线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
  • 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
  • 线程可以使用多核CPU,但是协程不可以。
  • 如果是 I/O 密集型,且 I/O 请求比较耗时的话,使用协程。
    如果是 I/O 密集型,且 I/O 请求比较快的话,使用多线程。
    如果是 计算 密集型,考虑可以使用多核 CPU,使用多进程。

三、三种爬虫方式的实验对比

本次实验是爬取豆瓣top250电影,输出电影的名字。
具体的分析我就不写了,主要是对比这三种方法的速度快慢

1.多进程

from multiprocessing import Process,Queue,Pool,Manager
import requests
import time
from bs4 import BeautifulSoup

def crawler_run(q,index):
    Process_id='Process--'+str(index)
    while not q.empty():
        num = q.get(timeout=2)
        soup = connec_douban(num)
        titles = soup.findAll("div",class_="item")#.find("span",class_="title").text
        for title in titles:
            print(Process_id,q.qsize(),title.find("span",class_="title").text+"\n")
def connec_douban(num1):
    link="https://movie.douban.com/top250?start="+str(num1)+"&filter="
    headers={
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'
    }
    re = requests.get(link,headers=headers)
    soup = BeautifulSoup(re.text,"lxml")
    print("url-status",re.status_code,end="\n")
    return soup


if __name__=='__main__':
    start = time.time()
    manager = Manager()
    p = Pool(4)
    q = manager.Queue(10)
    #q = Queue(10)
    for j in range(0,225,25):
        q.put(j)

    for i in range(4):
        p.apply_async(func=crawler_run,args=(q,i))
        print("Start Process",i)
    p.close()
    p.join()

    end = time.time()
    print('Pool+Queue 的多进程爬虫的总时间为',end-start)
    print("END")

2.多线程

import requests
from bs4 import BeautifulSoup
import time
import threading
import queue
class Mythreading(threading.Thread):
    def __init__(self,q,name):
        threading.Thread.__init__(self)
        self.q = q
        self.name=name
    def run(self):#这个run方法在进程创建后会自动运行这个程序
        # print("starting"+self.name)
        # print(self.q.qsize())
        # print(self.q.empty())
        while not self.q.empty():
            crawler_run(self.q,self.name)
        print("exiting...")
def crawler_run(q,name):
    num = q.get(timeout=2)
    soup = connec_douban(num)
    #print("run-run")
    titles = soup.findAll("div",class_="item")#.find("span",class_="title").text
    for title in titles:
        print(name,q.qsize(),title.find("span",class_="title").text+"\n")
def connec_douban(num1):
    link="https://movie.douban.com/top250?start="+str(num1)+"&filter="
    headers={
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'
    }
    re = requests.get(link,headers=headers)
    soup = BeautifulSoup(re.text,"lxml")
   # print("url-status",re.status_code,end="\n")
    return soup

if __name__=='__main__':
    threadset=[]
    start = time.time()
    q = queue.Queue(14)
    for i in range(0,326,25):
        q.put(i)
    ThreadNames=["process1","process2","process3","process4"]
    for i in range(0,4):
        p = Mythreading(q,ThreadNames[i])
        p.start()
        threadset.append(p)
    for thread in threadset:
        thread.join()
    end = time.time()
    print("总时间:",end-start)

3.多协程

import gevent
from gevent.queue import Queue,Empty
from gevent import monkey
monkey.patch_all()#将IO转化为异步执行的函数
import time
from bs4 import BeautifulSoup
import requests

link_list=[]
def crawler_run(index):
    processid = 'process-'+str(index)
    while not workqueue.empty():
        num = workqueue.get(timeout=2)
        soup = connec_douban(num)
        titles = soup.findAll("div",class_="item")#.find("span",class_="title").text
        for title in titles:
            print(processid,workqueue.qsize(),title.find("span",class_="title").text+"\n")
def connec_douban(num1):
    link="https://movie.douban.com/top250?start="+str(num1)+"&filter="
    headers={
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'
    }
    re = requests.get(link,headers=headers)
    soup = BeautifulSoup(re.text,"lxml")
    # print("url-status",re.status_code,end="\n")
    return soup
def boss():
    for i in range(0,226,25):
        workqueue.put_nowait(i)

if __name__=='__main__':
    start = time.time()
    workqueue = Queue(10)
    gevent.spawn(boss).join()
    jobs = []
    for i in range(4):
        jobs.append(gevent.spawn(crawler_run,i))
    gevent.joinall(jobs)
    end = time.time()
    print("总时间为",end-start)

三种方案我们均运行五次,取平均运行时间

方式 个数 时间
协程 4 1.3279s
线程 4 1.7486s
进程 4 3.1559s

由于这个实验的数据量不是很大,所以这个结果不具有代表性,但是也能够反映一些问题。

结论:针对数据量比较小,而且I/O比较频繁的,推荐使用协程。当然,你也可以将他们结合起来使用。

参考来源:
[1]:进程、线程、协程的理解
[2]:进程和线程、协程的区别
[3]:百度百科-协程
[4]:百度百科-线程
[5:]百度百科-进程

发布了48 篇原创文章 · 获赞 34 · 访问量 23万+

猜你喜欢

转载自blog.csdn.net/lzx159951/article/details/103504485