多线程与多进程 python爬虫进阶篇

借鉴:https://blog.csdn.net/qq_40244755/article/details/90043484

观前提示:因为python自身编辑器的原因,python多线程有时候甚至会降低效率,所以我们一般使用多进程而不是多线程,即用multiprocessing替代Thread multiprocessing库来弥补thread库因为GIL而低效的缺陷。本篇学习主要是为了探究线程进程的运行知识。

名词介绍

(不影响之后阅读,可跳过)

涉及名词参考:https://www.cnblogs.com/yuanchenqi/articles/6755717.html

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。

线程则是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)CPU分给线程,即真正在CPU上运行的是线程。

avatar
串行、并发与并行:
avatar
同步与异步

同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;

异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态,当有消息返回时系统会通知进程进行处理。

多线程

要完成一个多线程代码,我们将分以下步骤去进行:

①创建一个可供多个线程共享的数据队列
②准备好产生数据的函数(producer生产者)以及传入数据后对数据经行操作的函数(consumer消费者)
③创建线程,将生产者与消费者对应上
④运行产生数据的线程,将要传入的数据准备好
⑤通过for循环使消费函数多线程同时运行
⑥等待所有线程运行完毕

(以上为大体框架,下面为详细的实现介绍)

首先,我们要引入threading库去创造线程对象

t= Thread(target=countdown, args=(10,)) #创建线程对象

cutdown为我们要执行线程的函数名

args为我们要放入我们执行线程函数的传入变量,一般为组元类型

我们可以通过查看布尔值:t.is_alive()来判断该线程是否在运行

如果一个线程需要在后台运行,我们可以将他创立成后台线程:

t = Thread(target=countdown, args=(10,), daemon=True)

接下来,我们看一下一个完整线程的运行:

class CountDownTask:
    def __init__(self):
        self._running = True

    def terminate(self):
        self._running = False

    def run(self, n):
        while self._running and n > 0:
            print('T-minus', n)
            n -= 1
            time.sleep(5)

if __name__ == '__main__':
    c = CountDownTask()
    t = Thread(target=c.run, args=(10,))
    t.start() #运行这个线程
    c.terminate()  #结束这个线程
    t.join()  #表示等待该线程结束后再执行之后的代码

而将多个线程连在一起,为了保持各个线程之间的通讯,我们引入Queue库来创建一个能被多个线程共享的Queue队列,并运用put() 和 get() 方法来向队列中添加或者删除元素 。

put() 将数据存入队列 get()从队列中取出数据并将该数据从队列中删除

from queue import Queue
from threading import Thread
def producer(out_q):
    while True:
        out_q.put(1)
def consumer(in_q):
    while True:
        data = in_q.get()     
if __name__ == '__main__':
    q = Queue()
    t1 = Thread(target=consumer, args=(q, ))
    t2 = Thread(target=producer, args=(q, ))
    t1.start()
    t2.start()

此处的producer(生产者:产生数据)和consumer(消费者:引入数据进行相关操作)是两个不同的线程但是但是此处共享队列q 这样当生产者生产了数据后,消费者会拿到,然后消费它。(同步与异步)

同理,我们看下列一个简单的多线程爬虫程序

    def run(in_q, out_q):
        headers = {.....}
        while in_q.empty() is not True:#确认队列中还有数据没有被执行
            '''
            爬虫的爬取部分代码略
            注意:
            传入的url通过url=in_q.get()来获取
            最后的数据通过put方法放入out_q中(相当于list的append)

            '''
            in_q.task_done()#通知队列已完成任务
    if __name__ == '__main__':
    queue = Queue()#创建任务队列
    result_queue = Queue()#创建结果队列
    for i in range(1, 1001): #往队列中添加任务
       queue.put('http://www.g.com?page='+str(i))
    print('queue 开始大小 %d' % queue.qsize())
    for index in range(10):#使用十个线程来执行
            thread = Thread(target=run, args=(queue, result_queue, ))
            thread.daemon = True  # 随主线程退出而退出
            thread.start()
    queue.join()  # 队列消费完 线程结束
    print('queue 结束大小 %d' % queue.qsize())
    print('result_queue 结束大小 %d' % result_queue.qsize())

注意:因为queue的get()方法会删除队列中的数据,所以如果我们要看队列中的内容注意做好备份

while not queue.empty():
    item = queue.get()
    datem.append(item)
    print(item)

执行完这段程序后,原来的queue队列中的数据就已经完全被删完了

多线程以生产者和消费者格式写的完整代码如下:

from queue import Queue
import threading
from threading import Thread
import requests
from bs4 import BeautifulSoup
import re
import my_fake_useragent as ua
findname = re.compile(r'<span class="title">(.*?)</span>')
findscore = re.compile(r'<span class="rating_num" property="v:average">(.*?)</span>',re.S)
def producer(in_q):  # 生产者
    ready_list = []
    while in_q.full() is False:
        for i in range(0, 10):  # 往队列中添加任务
            url = 'https://movie.douban.com/top250?start=' + str(i * 25)
            if url not in ready_list:
                ready_list.append(url)
                in_q.put(url)
            else:
                continue
def consumer(in_q, out_q):  # 消费者
    header = {
        'User-Agent': ua.UserAgent().random()
    }
    while in_q.empty() is not True:  # 确认队列中还有数据没有被执行
        page = requests.get(url=in_q.get(), headers=header)
        html = page.text
        bs = BeautifulSoup(html, "html.parser")
        for item in bs.find_all('div', class_="item"):
            item = str(item)
            name = re.findall(findname, item)[0]
            score = re.findall(findscore, item)[0]
            out_q.put(str(threading.current_thread().getName()) + '::' + str(name) + '=' + str(score))
        in_q.task_done()
if __name__ == '__main__':
    queue = Queue(maxsize=10)  # 创建任务队列
    result_queue = Queue()  # 创建结果队列
    print('queue 开始大小 %d' % queue.qsize())
    producer_thread = Thread(target=producer, args=(queue,))
    producer_thread.daemon = True
    producer_thread.start()
    for index in range(10):
        consumer_thread = Thread(target=consumer, args=(queue, result_queue,))
        consumer_thread.daemon = True
        consumer_thread.start()
    queue.join()  # 队列消费完 线程结束
    print('queue 结束大小 %d' % queue.qsize())
    print('result_queue 结束大小 %d' % result_queue.qsize())
    while not result_queue.empty():
        item = result_queue.get()
        print(item)

其他可用于调试的库方法:

返回当前的线程变量:threading.currentThread()

用于返回线程的名字:threading.current_thread().getName()

返回正在运行的线程数量:threading.activeCount()

关于GIL锁:

获得锁:threadLock.acquire()

成功获得锁定后返回True,否则超时后将返回False。可选的timeout参数不填时将一直阻塞直到获得锁定

释放锁: threadLock.release()

多进程

多进程与多线程基本一致,其思路是创建多个进程并将他们放到进程池中,进程池调用多个核进行同时运算(理论上进程池的最大上限和CPU的核数有关)

我们直接来看代码理解:

import multiprocessing, requests,re, os
from bs4 import BeautifulSoup
import my_fake_useragent as ua

findname = re.compile(r'<span class="title">(.*?)</span>')
findscore = re.compile(r'<span class="rating_num" property="v:average">(.*?)</span>', re.S)

def consumer(in_q, out_q):  # 消费者
    header = {
        'User-Agent': ua.UserAgent().random()
    }
    while in_q.empty() is not True:  # 确认队列中还有数据没有被执行
        page = requests.get(url=in_q.get(), headers=header)
        html = page.text
        bs = BeautifulSoup(html, "html.parser")
        for item in bs.find_all('div', class_="item"):
            item = str(item)
            name = re.findall(findname, item)[0]
            score = re.findall(findscore, item)[0]
            print('%s :: %s = %s'%(os.getpid(),str(name),str(score)))
            out_q.put(str(os.getpid()) + '::' + str(name) + '=' + str(score))
        in_q.task_done()

if __name__ == '__main__':
    queue = multiprocessing.Manager().Queue()#多进程之间的通讯队列,实现进程间的数据共享
    #注:multiprocessing.Manager也可以设置共享列表或字典
    result_queue = multiprocessing.Manager().Queue()
    for i in range(0, 10):#producer
        url = 'https://movie.douban.com/top250?start=' + str(i * 25)
        queue.put(url)
    pool = multiprocessing.Pool(3)  # 创建异步进程池(非阻塞)设置最大同时运行线程数为3
    #注:这里的异步指的是启动子进程的过程,与父进程本身的执行(爬虫操作)是异步的
    for index in range(10):
        pool.apply_async(consumer, args=(queue, result_queue,))
        # 维持执行的进程总数为3,当一个进程执行完后启动一个新进程.
    pool.close()
    pool.join()
    queue.join()  # 队列消费完 线程结束
    print('queue 结束大小 %d' % queue.qsize())
    print('result_queue 结束大小 %d' % result_queue.qsize())

更多关于多进程

参考:https://www.liaoxuefeng.com/wiki/1016959663602400/1017628290184064

进程池批量创建子进程后运行的逻辑可以通过下列代码理解:

from multiprocessing import Pool
import os, time, random

def long_time_task(name):
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid)#查看现在父节点
    p = Pool(4)
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()
    print('All subprocesses done.')

运行结果如下:

Parent process 669.
Waiting for all subprocesses done...
Run task 0 (671)...
Run task 1 (672)...
Run task 2 (673)...
Run task 3 (674)...
Task 2 runs 0.14 seconds.
Run task 4 (673)...
Task 1 runs 0.27 seconds.
Task 3 runs 0.86 seconds.
Task 0 runs 1.41 seconds.
Task 4 runs 1.91 seconds.
All subprocesses done.

0、1、2、3同时执行,4滞后执行

如果子进程是外部进程,我们可以通过subprocess模块启动他

import subprocess
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)

如果子程序需要输入,我们可以通过communicate()方法输入

p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
output, err = p.communicate(b'set q=mx\npython.org\nexit\n')
print(output.decode('utf-8'))
posted @ 2021-01-27 21:05  Solmidola  阅读(136)  评论(1)    收藏  举报