【爬虫】使用多线程、多进程、多协程提升爬虫速度

本系列为自己学习爬虫的相关笔记,如有误,欢迎大家指正

要学习提升爬虫速度用到的知识,必须先熟悉并发和并行、同步和异步的概

一、并发和并行,同步和异步

并发和并行

并发(concurrency)和并行(parallelism)是两个相似的概念。并发是指在一个时间段内发生若干事件的情况,并行是指在同一时刻发生若干事件的情况。

使用单核CPU和多核CPU来说就是:在使用单核CPU时,多个工作任务是以并发的方式运行的,因为只有一个CPU,所以各个任务会分别占用CPU的一段时间依次执行。如果在自己分得的时间段没有完成任务,就会切换到另一个任务,然后在下一次得到CPU使用权的时候再继续执行,直到完成。在这种情况下,因为各个任务的时间段很短、经常切换,所以给我们的感觉是“同时”进行。在使用多核CPU时,在各个核的任务能够同时运行,这是真正的同时运行,也就是并行。

同步和异步

同步就是并发或并行的各个任务不是独自运行的,任务之间有一定的交替顺序,可能在运行完一个任务得到结果后,另一个任务才会开始运行。就像接力赛跑一样,要拿到交接棒之后下一个选手才可以开始跑。

异步则是并发或并行的各个任务可以独立运行,一个任务的运行不受另一个任务影响,任务之间就像比赛的各个选手在不同的赛道比赛一样,跑步的速度不受其他赛道选手的影响。

二、多线程爬虫

多线程爬虫是以并发的方式执行的。也就是说,多个线程并不能真正的同时执行,而是通过进程的快速切换加快网络爬虫速度的。

在Python设计之初,为了数据安全所做的决定设置有GIL(Global Interpreter Lock,全局解释器锁)。在Python中,一个线程的执行过程包括获取GIL、执行代码直到挂起和释放GIL。在一个Python进程中,只有一个GIL,拿不到GIL的线程就不允许进入CPU执行。

正因为如此,在多核CPU上Python的多线程效率也不高。因为每次释放GIL锁,线程之间都会进行锁竞争,而切换线程会消耗资源。

虽然如此,但是因为网络爬虫是IO密集型,线程能够有效地提升效率,因为单线程下有IO操作会进行IO等待,所以会造成不必要的时间浪费,而开启多线程能在线程A等待时自动切换到线程B,可以不浪费CPU的资源,从而提升程序执行的效率。

Python的多线程对于IO密集型代码比较友好

使用单线程爬虫

import requests
import time
link_list = []

with open('most.txt') as file:
    file_list = file.readlines()
    for each in file_list:
        link = each.split()[1]
        link = link.replace('\n','')
        link_list.append(link)
    start = time.time()
    for eachone in link_list:
        try:
            r = requests.get(eachone)
            print(r.status_code,eachone)
        except Exception as e:
            print('Error:',e)
    end = time.time()
    print('串行时间:',end-start)

使用多线程

在python中使用多线程有两种方法

1.函数式

调用_thread模块中的start_new_thread()函数产生新线程

简单示例

import _thread
import time
# 为线程定义一个函数
def print_time(threadName,delay):
    count = 0
    while count < 3:
        time.sleep(delay)
        count += 1
        print(threadName,time.ctime())

_thread.start_new_thread(print_time,('Thread-1',1))
_thread.start_new_thread(print_time,('Thread-2',2))

#time.sleep(5)
print('Main Finished')

这个代码没出现自己想要的结果,暂时没看出来问题在哪。

  • _thread中使用start_new_thread ()函数来产生新线程

    def start_new_thread(function, args, kwargs=None): 
    
  • function表示线程函数

  • args为传递给线程函数的参数,它必须是tuple类型

2.类包装式

调用Threading库创建线程,从threading.Thread继承

threading模块提供了Thread类来处理线程,包括以下方法。

  • run():用以表示线程活动的方法。
  • start():启动线程活动。
  • join([time]):等待至线程中止。阻塞调用线程直至线程的join()方法被调用为止。
  • isAlive():返回线程是否是活动的。
  • getName():返回线程名。
  • setName():设置线程名。

示例:

import threading
import  time

class myThread(threading.Thread):
    def __init__(self,name,delay):
        threading.Thread.__init__(self)
        self.name = name
        self.delay = delay
    def run(self):
        print('Starting:'+self.name)
        print_time(self.name,self.delay)
        print('Exiting:'+self.name)
def print_time(threadName,delay):
    counter = 0;
    while counter<3:
        time.sleep(delay)
        print(threadName,time.ctime())
        counter += 1

threads = []
# 创建新线程
thread1 = myThread('Thread-1',1)
thread2 = myThread('Thread-2',2)
# 开启新线程
thread1.start()
thread2.start()

# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)

for t in threads:
    t.join()
print('Exiting Main Thread')

2.1简单的多线程爬虫

import threading
import requests
import time

link_list = []

with open('most.txt','r') as file:
    file_list = file.readlines();
    for eachone in file_list:
        link = eachone.split()[1]
        link = link.replace('\n','')
        link_list.append(link)

start = time.time()

class myThread(threading.Thread):
    def __init__(self,name,link_range):
        threading.Thread.__init__(self)
        self.name = name
        self.link_range = link_range

    def run(self):
        print('starting '+self.name)
        crawler(self.name,self.link_range)
        print('exiting '+self.name)

def crawler(threadName,link_range):
    for i in range(link_range[0],link_range[1]+1):
        try:
            r = requests.get(link_list[i],timeout=20)
            print(threadName,r.status_code,link_list[i])
        except Exception as e:
            print(threadName,'Error: ' ,e)

link_range_list = [(0,200),(201,400),(401,600),(601,800),(801,1000)]
thread_list = []

# 创建新线程
for i in range(1,6):
    thread = myThread('Thread-'+str(i),link_range_list[i-1])
    thread.start()
    thread_list.append(thread)

# 等待所有线程完成
for thread in thread_list: 
    thread.join() #thread.join()方法等待各个线程执行完毕

end = time.time()

print('简单多线程爬虫的总时间为:',end-start)
print('Exiting Main Thread')

这个代码还是有改进的余地,比如说某一个线程中的200个已经结束了,那么就还剩4个线程。到最后就可能变成单线程了。

2.2 queue多线程爬虫

import threading
import  time
import requests
import queue as Queue


link_list = []
with open('most.txt','r') as file:
    file_list = file.readlines()
    for eachone in file_list:
        link = eachone.split()[1]
        link = link.replace('\n','')
        link_list.append(link)

start = time.time()

class myThread(threading.Thread):
    def __init__(self,threadName,q):
        threading.Thread.__init__(self)
        self.name = threadName
        self.q=q
    def run(self):
        print('starting '+self.name)
        while True:
            try:
                crawler(self.name,self.q)
            except:
                break
        print('exit '+self.name)

def crawler(threadName,q):
    url = q.get(timeout=2)
    try:
        r = requests.get(url,timeout=20)
        print(q.qsize(),threadName,r.status_code,url)
    except Exception as e:
        print(q.qsize(), threadName, url, 'Error:',e)
thread_list = ['Thread-1','Thread-2','Thread-3','Thread-4','Thread-5']
workQueue = Queue.Queue(1000)
threads = []
# 创建新线程
for tName in thread_list:
    thread = myThread(tName,workQueue)
    thread.start()
    threads.append(thread)
#填充队列
for url in link_list:
    workQueue.put(url)

# 等待所有线程完成
for t in threads:
    t.join()

end = time.time()
print('Queue多线程爬虫的总时间:'+end-start)
print('Exiting Main Thread')

Python的多线程爬虫只能运行在单核上,各个线程以并发的方法异步运行。由于GIL(Global Interpreter Lock,全局解释器锁)的存在,多线程爬虫并不能充分地发挥多核CPU的资源。

三、多进程爬虫

多进程爬虫则可以利用CPU的多核,进程数取决于计算机CPU的处理器个数。由于运行在不同的核上,各个进程的运行是并行的。在Python中,如果我们要用多进程,就需要用到multiprocessing这个库。

使用multiprocess库的两种方法

当进程数量大于CPU的内核数量时,等待运行的进程会等到其他进程运行完毕让出内核为止。因此,如果CPU是单核,就无法进行多进程并行。在使用多进程爬虫之前,我们需要先了解计算机CPU的核心数量

from multiprocessing import cpu_count
print(cpu_count())

1.使用Process + Queue的多进程爬虫

from multiprocessing import cpu_count
from multiprocessing import Process,Queue
import time
import requests
link_list = []
print(cpu_count())

with open('most.txt','r') as file:
    file_list = file.readlines()
    for eachfile in file_list:
        link = eachfile.split()[1]
        link = link.replace('\n','')
        link_list.append(link)

start = time.time()
class MyProcess(Process):
    def __init__(self,q):
        Process.__init__(self)
        self.q = q

    def run(self):
        print('starting:' ,self.pid)
        while not  self.q.empty():
            crawler(self.q)
        print('exit:',self.pid)



def crawler(q):
    url = q.get(timeout = 2)
    try:
        r = requests.get(url,timeout=20)
        print(q.qsize(),r.status_code,url)
    except Exception as e:
        print(q.qsize(),url,'Error:',e)

if __name__ =='__main__':
    ProcessNames = ['Process-1','Process-2','Process-3','Process-4','Process-5']
    workQueue = Queue(1000)
    # 填充队列
    for url in link_list:
        workQueue.put(url)
    for i in range(0,3):
        p = MyProcess(workQueue)
        p.daemon = True #如果将daemon设置为True,当父进程结束后,子进程就会自动被终止。
        p.start()
        p.join()
    end = time.time()
    print('Process + Queue多进程爬虫的总时间为:',end-start)

    print('Main process Ended!')

2.使用Pool + Queue的多进程爬虫

from multiprocessing import Pool,Manager
import time
import requests
link_list = []
with open('most.txt','r') as file:
    file_list = file.readlines()
    for each in file_list:
        link = each.split()[1].replace('\n','')
        link_list.append(link)

start = time.time()
def crawler(q,index):
    Process_id = 'Process-'+str(index)
    while not  q.empty():
        url = q.get(timeout=2)
        try:
            r = requests.get(url,timeout=20)
            print(Process_id,q.qsize(),r.status_code,url)
        except Exception as e:
            print(Process_id,q.qsize(),url,'Error:',e)

if __name__ == '__main__':
    manager = Manager()
    workQueue = manager.Queue(1000)
    # 填充队列
    for url in link_list:
        workQueue.put(url)
    pool = Pool(processes=3) #使用Pool(processes=3)创建线程池的最大值为3
    for i in range(4):
        pool.apply_async(crawler,args=(workQueue,i)) # 创建子进程 这里采用的是非阻塞方法
    print('Start process')
    pool.close()
    pool.join()
    end = time.time()
    print('Pool +Queue多进程爬虫的总时间为:',end-start)
    print('Main process End')

四、多协程(Coroutine)爬虫

使用协程的好处:

  • 协程像一种在程序级别模拟系统级别的进程,由于是单线程,并且少了上下文切换,因此相对来说系统消耗很少
  • 协程方便切换控制流,这就简化了编程模型。协程能保留上一次调用时的状态(所有局部状态的一个特定组合),每次过程重入时,就相当于进入了上一次调用的状态。
  • 协程的高扩展性和高并发性,一个CPU支持上万协程都不是问题,所以很适合用于高并发处理。

协程的缺点:

  • 协程的本质是一个单线程,不能同时使用单个CPU的多核,需要和进程配合才能运行在多CPU上
  • 有长时间阻塞的IO操作时不要用协程,因为可能会阻塞整个程序

在python的协程中可以使用gevent库

pip install gevent

import gevent

from gevent.queue import Queue,Empty
import time

#把下面有可能有IO操作的单独坐上标记
"""
以下两行,可以实现爬虫的并发能力,如果没有这两句的话,整个抓取过程就会变成
依次抓取gevent库中的monkey能把可能有IO操作的单独做上标记,将IO变成可以异步执行的函数
"""
from gevent import monkey
monkey.patch_all()#将IO转为异步执行的函数
import requests
link_list = []

with open('most.txt','r') as file:
    file_list = file.readlines()
    for each in file_list:
        link_list.append(each.split()[1].replace('\n',''))

start = time.time()

def crawler(index):
    Process_id = 'Process-'+str(index)
    while not workQueue.empty():
        url = workQueue.get(timeout=2)
        try:
            r = requests.get(url,timeout=20)
            print(Process_id,workQueue.qsize(),r.status_code,url)
        except Exception as e:
            print(Process_id, workQueue.qsize(),  url,'Error',e)

def boss():
    for url in link_list:
        workQueue.put_nowait(url)
if __name__ =='__main__':
    workQueue = Queue(1000)
    gevent.spawn(boss).join() # 将队列中加入的内容整合到gevent中
    '''
    下面4行是创建多协程爬虫的程序
    '''
    jobs = []
    for i in range(10): #创建10个协程
        jobs.append(gevent.spawn(crawler,i))
    gevent.joinall(jobs)
    end = time.time()
    print('多协程爬虫的总时间为:',end-start)
    print('Main Ended')

五、总结

  1. 并发(concurrency)和并行(parallelism):并发是指在一个时间段发生若干事件的情况。并行是指在同一时刻发生若干事件的情况

  2. 同步是指并发或并行的各个任务不是独自运行的,任务之间有一定的交替顺序,可能在执行完一个任务并得到结果后,另一个任务才会开始运行。

  3. 异步则是并发或并行的各个任务可以独立运行,一个任务的运行不受另一个影响。

  4. 多线程的方式:
    【爬虫】使用多线程、多进程、多协程提升爬虫速度_第1张图片

    程序的执行是在不同线程之间切换的。当一个线程等待网页下载时,进程可以切换到其他线程执行。

  5. 多进程的方式
    【爬虫】使用多线程、多进程、多协程提升爬虫速度_第2张图片

    程序的执行是并行、异步的,多个线程可以在同一时刻发生若干事件。

  6. 多协程的执行方式
    【爬虫】使用多线程、多进程、多协程提升爬虫速度_第3张图片

    协程是一种用户态的轻量级线程,在程序级别来模拟系统级别用的进程。在一个进程中,一个线程通过程序的模拟方法实现高并发。

    微信搜一搜【梓莘】或扫描下方二维码交个朋友共同进步。文章持续更新中。目前在整理爬虫相关学习笔记,期待后续更多的更新【爬虫】使用多线程、多进程、多协程提升爬虫速度_第4张图片
    【爬虫】使用多线程、多进程、多协程提升爬虫速度_第5张图片

你可能感兴趣的:(爬虫,python,多线程,爬虫)