python3爬虫系列15之爬虫增速多线程,线程池,队列的用法(通俗易懂)

python3爬虫系列15之爬虫增速多线程,线程池的用法(通俗易懂)

1.前言

在上一篇文章中,我们了解了一些 python 高效爬虫的概念,
python3爬虫系列14之进程、单进程、多进程、线程、单线程、多线程、并行、并发、互斥锁、协程的白话解释,这一系列。

那么我们这一篇就开始了解多线程的使用介绍。

现在的 PC 都是多核的,使用多线程能充分利用 CPU 来提供程序的执行效率。

每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。一个进程可包含多个线程,其中有且只有一个主线程。

2.python的多线程模块

在 python 中常用的多线程的模块有这么几个:

  1. _thread
  2. threading
  3. Queue

py2的时候,有个 thread 模块,被 python3 改名为 _thread 模块,但其实 _thread 也没什么人用。

其实 _thread 也没什么人用,因为 _thread 有的 threading 都有,_thread 没有的 threading 依然有,所以推荐学习threading 。

python3爬虫系列15之爬虫增速多线程,线程池,队列的用法(通俗易懂)_第1张图片

2.1 线程—threading 的使用

(自从我看了一篇白话的写作风格以后,博主觉得很有趣,后面就想尝试这样写了。因为一味的搬运概念解释的太多了。)

在这里介绍一下自己,博主是某工地的搬砖工。

【任务:博主臭弟弟每天工作得搬20块砖,每隔一秒钟搬一块。累死了~】

用代码实现如下:

#!/usr/bin/python3
#@Readme : 1.无线程举例

import time
def job_time(name, delay, counter):
    while counter:
        time.sleep(delay)
        print("%s 开始搬第%d块砖 %s" % (name,counter, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))
        counter -= 1

if __name__ == '__main__':
    job_time('鼠标',1,20)

搬砖开始:

python3爬虫系列15之爬虫增速多线程,线程池,队列的用法(通俗易懂)_第2张图片

搬砖好辛苦啊,昨天博主学习了线程的知识,于是知道了多线程。

那么如果博主可以开启外挂,是不是就可以轻松一些呢?python3爬虫系列15之爬虫增速多线程,线程池,队列的用法(通俗易懂)_第3张图片

可以使用多线程啊。

2.1.1 多线程threading 的使用

多个threading组成多线程——搬砖拉上几个小伙伴,一起搬,它不香嘛?

说干就干.

假设把朋友小明和小红拉来,让他们一起帮我搬砖。

也就是让小明和小红同时一人搬10块砖,这就省去一半的工作了。O(∩_∩)O哈哈哈~

【任务:让小明和小红共同搬10块砖,小红每搬1块砖休息 2 秒,小明每搬1块休息 1 秒,美滋滋~】

用代码实现如下:

#!/usr/bin/python3
#@Readme : 2.多线程做法

import threading
import time

# 创建一个线程类,然后继承 threading.Thread
class MyThread(threading.Thread):
    def __init__(self,threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    # 定义run方法,调用了搬砖的方法-也就是我们要做的事情
    def run(self):
        print("开始线程:" + self.name)
        job_time(self.name, self.counter, 10)
        print("退出线程:" + self.name)
def job_time(threadName, delay, counter):
    while counter:
        time.sleep(delay)
        print("%s 开始搬第%d块砖 %s" % (threadName,counter, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))
        counter -= 1

# 创建了两个线程
def Create_thread():
    # 找了两个人来搬砖-让小明和小红同时一人搬 10 块砖.
    thread1 = MyThread(1, "小明", 1)  # 让小明搬一次砖休息1秒钟
    thread2 = MyThread(2, "小红", 2)  # 让小红搬一次砖休息2秒钟
    # 开启新线程-就会去调用start方法-接着执行run方法
    thread1.start()
    thread2.start()
    # 等待至线程中止-再终止主程序
    thread1.join()
    thread2.join()
    print("退出主线程")
if __name__ == '__main__':
    Create_thread()

效果:
python3爬虫系列15之爬虫增速多线程,线程池,队列的用法(通俗易懂)_第4张图片
上诉实际上就是一个多线程的例子,解释一下:

1.首先我们呢创建了一个线程类class MyThread(threading.Thread):

2.然后继承 threading.Thread。

3.然后在这个线程类里面定义了一个 run 方法,

4.这个 run 方法去调用了搬砖的方法

其中我们创建了两个线程:

一个叫小明线程: thread1 = MyThread(1, "小明", 1)  # 休息1秒
一个叫小红线程: thread2 = MyThread(2, "小红", 2)  #休息2秒

当我们的线程调用 start 方法的时,它们就会去执行 run 方法,然后执行join方法,join 方法:是为了让线程执行完毕,再终止主程序。


好了,这么一个多线程的搬砖任务就结束了,那么如果我需要多几个人帮忙或者任务完成更快,就可以需要几个线程就再加几个。
thread1 = MyThread() # 这个地方,我们需要几个线程就再加几个,

比如

thread5 = MyThread(55, "张三", 2)
thread6 = MyThread(56, "李四", 5).....更多

但是!!!问题来了,搬砖完毕了,请吃饭花钱太多吧,一人请一顿划不来吧?
也就是
这样频繁的线程创建,销毁线程,是非常浪费资源的,所以我们应该把他们放到一个容器里面去统一管理。

而这个容器,称为——线程池

3. 线程池—ThreadPoolExecutor的使用

通过线程池就可以重复利用线程,这样就不会造成过多的浪费了。(一次性请搬砖的大伙吃饭,就省去好多Q了~~)

在 python 中可以使用 ThreadPoolExecutor 来实现线程池。

【比如,我们往池子里塞 20 个线程(就是请了20个朋友吃饭),然后在循环的时候每次拿一个线程(朋友)来搬砖。】

线程池用代码实现如下:

#!/usr/bin/python3

'''
#@Readme : 3.线程池的例子
在创建线程的时候,我们虽然可以需要几个就加几个,但是这样频繁的创建,
销毁线程,是非常的浪费资源。
所以提倡把他们放到池子里面去统一管理——这就是线程池。ThreadPoolExecutor
比如往池子里塞20个线程,然后在循环的时候每次拿一个线程来搬砖
'''

from concurrent.futures import ThreadPoolExecutor
import time

# 我们要做的事情
def job_time(name, delay, counter):
  while counter:
    time.sleep(delay)
    print("%s 开始搬第%d块砖 %s" % (name, counter, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))
    counter -= 1

if __name__ == '__main__':
  pool = ThreadPoolExecutor(20)       # 创建线程池,包含20个线程
  for i in range(1,21):                # 循环拿一个线程来搬20块砖,这样就不会去重复的创建销毁线程了
    pool.submit(job_time('鼠标'+str(i),1,3))    # 让线程搬一次砖休息3秒钟

效果:
python3爬虫系列15之爬虫增速多线程,线程池,队列的用法(通俗易懂)_第5张图片

在上诉代码中,我们使用ThreadPoolExecutor(20) 创建了一个线程池,里面包含20个线程。然后把这些线程,循环的方式提交去作业pool.submit,这样就不会去重复的创建销毁线程了。

使用线程池的这种方式,既节约了成本,又满足了工作。美滋滋~~~

4. 线程池—Queue队列的使用

当然了,请客也不一定只请吃饭嘛,也可以适当烤烤肉。

所以,创建线程池也不光光只有ThreadPoolExecutor类可以,我们还可以用一个叫做 Queue 的队列来创建线程池

Queue 就是队列嘛。(还记得大学数据结构吗?队列先进先出,栈先进后出。)

可以这样理解,

“队列”就是排队,队尾有人进入队列,另一头有人离开队列,即一端插入,一端删除;先排队的人先离开,即先进先出。

“栈”就是客栈,只能从门进出,即只在一端进行插入删除操作;进去了的人要想出来,就得门口的先出去,即先进后出。

速度不同:

栈只能从头部取数据,也就最先放入的需要遍历整个栈最后才能取出来,而且在遍历数据的时候还得为数据开辟临时空间,保持数据在遍历前的一致性。

队列则不同,它基于地址指针进行遍历,而且可以从头或尾部开始遍历,但不能同时遍历,无需开辟临时空间,因为在遍历的过程中不影像数据结构,速度要快的多。

咳咳,貌似跑偏了~~~

python3爬虫系列15之爬虫增速多线程,线程池,队列的用法(通俗易懂)_第6张图片

说回咱们的队列。

python在使用队列的时,最常用的方法就是 put (塞)和 get (取)了。

【比如,现在我创建一个长度为 10 的队列,紧接着根据队列的长度创建线程,然后每个线程都让它们处于守护状态(也就是需要的时候,马上执行)】

Queue线程池代码如下:

import threading
from queue import Queue
import time

# 4.使用Queue队列来创建线程池
# 最常用的方法是put和get

class CustomThread(threading.Thread):
    # 初始化属性
    def __init__(self, queue,number):
        threading.Thread.__init__(self)
        self.__queue = queue
        self.number = number                # 线程编号

    # 要执行的话就需要去队列里面取了get
    def run(self):
        print('启动%d号线程' % self.number)
        while True:
            q_method = self.__queue.get()
            q_method()
            self.__queue.task_done()

# 创建线程池-Queue队列
def queue_pool():
    queue = Queue(10)                     # 创建一个长度为10的队列
    for i in range(queue.maxsize):       # 根据队列的长度创建了线程
        t = CustomThread(queue,i+1)      # 初始化
        t.setDaemon(True)               # 每个线程都设置为守护状态=即需要时马上执行
        t.start()                       # 启动线程
    for i in range(20):
        queue.put(job)               # put方法=把我们想做的事情往队列里面塞
    queue.join()

# 想做的事情
def job():
    print(" 开始搬砖 %s" % (time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))

if __name__ == '__main__':
    queue_pool()

运行一下:

python3爬虫系列15之爬虫增速多线程,线程池,队列的用法(通俗易懂)_第7张图片
总体思路就是:

我们创建一个长度为 6 的队列——任务箱子

接着根据队列的长度创建了线程——搬砖人员

每个线程都让它们处于守护状态——工地等待

也就是需要的时候马上执行——需要时就顶上

接着我们就可以用 put 方法,把我们想做的事情往队列里面塞——想搬砖,存放搬砖任务。

比如我们想要搬砖了,要执行的话就需要去队列里面 get 取得搬砖任务,开搬。

这样愉快的一天就又过去了~~~
python3爬虫系列15之爬虫增速多线程,线程池,队列的用法(通俗易懂)_第8张图片


好了,到这里总结一下,我们今天主要是学习的就是:

  • threading线程模块,用它来创建多个线程。
  • ThreadPoolExecutor线程池模块,用它来创建一个管理多组线程的线程池。
  • Queue队列线程池,用它来创建一个管理多组线程的线程池。

补充方法

计算时间的方法:

start_time = time.time()
print("danxianc time is: {}".format(time.time() - start_time))

或者:

start = time.time()
main()             # 要调用的程序
end = time.time()
spendtime = end - start
print("用时 " + str(spendtime) + "秒")

可以用来计算你的爬虫程序,爬取花了多少时间。

下一篇,用多线程来爬取网站咯。喜欢您就点个赞吧~

你可能感兴趣的:(python爬虫系列)