爬虫:程序_进程_线程_多线程(案例多线程下载壁纸)


前言

  多线程的存在,不是为了提高程序的执行速度,而是为了提高程序的使用率和cpu利用率。
程序执行是为了抢到cpu的执行权,线程越多的进程,抢到执行权的几率越大

  多进程是电脑可以同时聊天、听歌、打游戏,但这不是并行执行,cpu一个时间只能做一件事情,是因为cpu在多个任务来回切换,属于并发执行,切换速度快给人的感觉就像是同时执行


一、什么是程序_进程_线程?

程序:用于实现一定功能的编程指令集合

进程:电脑启动后的程序称为进程,系统会为进程分配内存空间。电脑中时会有很多单独运行的程序,每个程序有一个独立的进程,而进程之间是相互独立存在的。(使用ctrl+alt+delete打开任务管理器查看进程) 

线程:

进程想要执行任务就需要依赖线程。换句话说,就是进程中的最小执行单位就是线程,并且一个进程中至少有一个线程。线程是cpu调整执行的基本单元,一个进程中包含n个线程,进程结束线程一定结束,线程结束但进程不一定结束,同一个进程的多个线程共享内存地址。

进程就像一条多跑道的马路而线程就是马路上的车辆,一条马路上有多个车辆同时行驶。

二、实现多线程的方法

  • 继承方式实现多线程

继承threading.Thread类

实现run()方法

调用线程Thread类的start()方法启动线程

        def mult():

               t1=codingThread()

               t1.start

最后运行主函数

if __name__ == '__main__':

          mult()

 代码如下(示例):

import threading
import time
class CodingThread(threading.Thread):           #继承threading.Thread 类
    def run(self):                              #实现run方法
        #print(threading.current_thread())      # 获取当前线程对象
        #thread=threading.current_thread()
        #print('线程的名称:',threading.current_thread().getName())    #获取线程的名称
        #thread.setName('我的线程')               #设置线程的名称
        for i in range(5):
            print('--fun1中i的值为:', i)
            time.sleep(1)
class CodingThread2(threading.Thread):          #继承第二个threading.Thread 类
    def run(self):
        print(threading.current_thread())       # 获取当前线程对象
        for i in range(5):
            print('-------fun2中i的值为:', i)
            time.sleep(1)
def mult():
    t1=CodingThread()
    t2=CodingThread2()
    t3=CodingThread2()
    t1.start()
    t2.start()
    t3.start()
    #print(threading.enumerate())               #获取当前运行的N多线程信息
if __name__ == '__main__':
    mult()
  • 为什么要是用类的方式创建线程:

      因为类可以更好地管理我们的代码,可以让我们使用面对对象的方式编程

  • 线程的一些常用方法:

代码用途
threading.current_thread()获取当前线程对象
threading.enumerate()获取当前运行的N多线程信息
threading.getName()获取线程名称
threading.setName设置线程名称

三.多线程访问全局变量的安全性问题:

  • 什么是线程安全:

当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。
既然是线程安全问题,那么毫无疑问,所有的隐患都是在多个线程访问的情况下产生的,也就是我们要确保在多条线程访问的时候,我们的程序还能按照我们预期的行为去执行,我们看一下下面的模拟车站买票代码。

代码示例:

import threading
import time
ticket=100
def sale_ticket():
    global ticket
    for i in range(1000):
        if ticket>0:
            print(threading.current_thread().getName()+'--》正在出售第{}张票'.format(ticket))
            ticket-=1
        time.sleep(1)

def start():
    for i in range(3):
        t=threading.Thread(target=sale_ticket)
        t.start()

if __name__ == '__main__':
    start()

在运行代码是会发现,一张票(数据)会在不同的售票口(线程)进行售卖,此时出现这种情况显然表明这个方法根本就不是线程安全的,出现这种问题的原因有很多。

最常见的一种,就是我们A线程在进入方法后,拿到了ticket的值,刚把这个值读取出来,还没有改变ticket的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的ticket值是一样的。

那么由此我们可以了解到,这确实不是一个线程安全的类,因为他们都需要操作这个共享的变量。其实要对线程安全问题给出一个明确的定义,还是蛮复杂的,我们根据我们这个程序来总结下什么是线程安全。

当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

四、解决线程安全性问题

  • 锁机制解决线程的安全性问题

加锁的操作代码
创建锁对象threading.lock
加锁threading.acquire
释放锁threading.release

在线程取数据时将数据加锁,当一个线程在处理数据时,数据处于封锁状态只有当锁被释放时下一个线程才能继续运行处理数据

代码演示(示例)

import threading
import time
ticket=100
#创建锁对象
lock=threading.Lock()
def sale_ticket():
    global ticket
    for i in range(100):
        lock.acquire()                  #加锁(上锁操作)
        if ticket>0:
            print(threading.current_thread().getName()+'--》正在出售第{}张票'.format(ticket))
            ticket-=1
            time.sleep(0.01)
            lock.release()
        else:
            print('已售完')
            break
                 #释放锁

def start():
    for i in range(3):
        t=threading.Thread(target=sale_ticket)
        t.start()

if __name__ == '__main__':
    start()
  • 生产者与消费者模式

import threading
import random
import time
gmoney=0                #共享资源
lock=threading.Lock()  #创建锁对象(保证money的安全性数据的正确性)
class Produce(threading.Thread):              #生产者类
    def run(self):
        global gmoney
        for _ in range(10):            #只要循环次数,不需要使用迭代对象进行操作
            lock.acquire()
            money=random.randint(1000,10000)
            gmoney+=money
            print(threading.current_thread().getName(),'挣了{}钱,当前余额为{}'.format(money,gmoney))
            #time.sleep(0.1)
            lock.release()


class Customer(threading.Thread):              #消费者类
    def run(self):
        global gmoney
        for _ in range(50):
            lock.acquire()
            money = random.randint(1000, 10000)
            if money <= gmoney:
                print(threading.current_thread().getName(), '花了{}钱,当前余额为{}'.format(money, gmoney))
                gmoney -= money
            else:
                print(threading.current_thread().getName(), '想花{}钱,但是余额不足,当前余额为{}'.format(money, gmoney))
            # time.sleep(0.1)
            lock.release()


def mult():
    for i in range(5):
        th=Produce(name='生产者{0}'.format(i))
        th.start()

    for i in range(5):
        cust=Customer(name='------消费者{}'.format(i) )
        cust.start()


if __name__ == '__main__':
    mult()

  • 线程之间的通信 

condition版的生产者有消费者模式

acquire()上锁
release()解锁
wait()将当前线程处于等待状态,并且释放锁,可以被其他线程使用,被唤醒后继续等待上锁,上锁后继续执行代码
notify()通知唤醒某个正在等待的线程
notify_all()通知唤醒所有正在等待的线程

  • queue线程安全队列 

from queue import Queue #FIFO队列,先进先出
q=Queue(5)              #创建一个队列,最多可以存放五个数据
#开始向队列中存放数据
for i in range(4):
    q.put(i)
print('队列中实际数据为多少:',q.qsize())

#取出数据
for _ in range(5):
    try:
        print(q.get(block=False))       #block  取完数据之后不用继续等待循环运行结束
    except:
        print('数据已全部取完,队列目前为空')
        break



if q.full():
    print('队列已满')
else:
    print('队列当前的数据个数为:',q.qsize(),'队列不满')

print('------------------')

小案例:多线程下载王者荣耀壁纸:

  • 获取链接(每张图片的链接)

import requests
from urllib import parse
headers={
'user-agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36',
'referer': 'https://pvp.qq.com/'
}

def exact_url(data):                  #提取一个图片内部图片八个不同大小的url
    image_url_lst=[]
    for i in range(1,9):
        image_url=parse.unquote(data['sProdImgNo_{}'.format(i)]).replace('200','0')          #将获取的url进行解析
        image_url_lst.append(image_url)
    return image_url_lst
def send_requests():
    url='https://apps.game.qq.com/cgi-bin/ams/module/ishow/V1.0/query/workList_inc.cgi?activityId=2735&sVerifyCode=ABCD&sDataType=JSON&iListNum=20&totalpage=0&page=1&iOrder=0&iSortNumClose=1&_everyRead=true&iTypeId=2&iFlowId=267733&iActId=2735&iModuleId=2735&_=1670142334580'
    #将其中的jsoncall那串数据删除,使得到的数据直接为json格式,不用删除替换前缀
    resp=requests.get(url,headers=headers)                     #判断是什么请求是不是ajks请求。查找到正确的url,并获取数据(图片对应的网址)
    print(resp.text)
    return resp.json()          #转换成json? 格式
def parse_json(json_data):
    d={}
    data_lst=json_data['List']  #在resp.json()中查找List
    #print(data_lst)
    for data in data_lst:
        image_url_lst=exact_url(data)          #调用函数获取八张大小不同图片的url
        sProdName=parse.unquote(data['sProdName'])
        d[sProdName]=image_url_lst
    for item in d:
        print(item,d[item])
def start():
    json_data=send_requests()
    parse_json(json_data)
if __name__ == '__main__':
    start()
  • 单线程下载王者荣耀壁纸:

import requests
from urllib import parse
import os
from urllib import request
headers={
'user-agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36',
'referer': 'https://pvp.qq.com/'
}

def exact_url(data):                  #提取内部图片不同大小的url
    image_url_lst=[]
    for i in range(1,9):
        image_url=parse.unquote(data['sProdImgNo_{}'.format(i)]).replace('200','0')          #将获取的url进行解析
        image_url_lst.append(image_url)
    return image_url_lst
def send_requests():
    url='https://apps.game.qq.com/cgi-bin/ams/module/ishow/V1.0/query/workList_inc.cgi?activityId=2735&sVerifyCode=ABCD&sDataType=JSON&iListNum=20&totalpage=0&page=1&iOrder=0&iSortNumClose=1&_everyRead=true&iTypeId=2&iFlowId=267733&iActId=2735&iModuleId=2735&_=1670142334580'
    #将其中的jsoncall那串数据删除
    resp=requests.get(url,headers=headers)
    #print(resp.text)
    return resp.json()          #转换成json 格式
def parse_json(json_data):
    d={}
    data_lst=json_data['List']
    for data in data_lst:
        image_url_lst=exact_url(data)
        sProdName=parse.unquote(data['sProdName'])
        d[sProdName]=image_url_lst
    '''for item in d:
        print(item,d[item])'''
    save_jpg(d)

def save_jpg(d):
    for key in d:  #拼接路径
         dirpath=os.path.join('image',key.strip(' '))   #拼接文件名
         os.mkdir(dirpath)            #创建文件夹
    #下载图片并进行保存
         for index,image_url in enumerate(d[key]):
             request.urlretrieve(image_url,os.path.join(dirpath,'{}.jpg').format(index+1))
             print('{}下载完毕'.format(d[key][index]))


def start():
    json_data=send_requests()
    parse_json(json_data)
if __name__ == '__main__':
    start()
  • 多线程下载王者荣耀壁纸(可以感受与单线程的区别):

import requests
from urllib import request
from urllib import parse
from queue import Queue
import threading
import os

headers={
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36',
'referer': 'https://pvp.qq.com/web201605/herolist.shtml'
}
def exact_url(data):                  #提取内部图片不同大小的url
    image_url_lst=[]
    for i in range(1,9):
        image_url=parse.unquote(data['sProdImgNo_{}'.format(i)]).replace('200','0')          #将获取的url进行解析
        image_url_lst.append(image_url)
    return image_url_lst
#生产者线程
class Producer(threading.Thread):
    def __init__(self,page_queue,image_url_queue):       ##定义生产者对象中的变量
        super().__init__()
        self.page_queue=page_queue
        self.image_url_queue=image_url_queue
    def run(self):
        while not self.page_queue.empty():            #只要self.page_queue不为空就要一直取url
            page_url=self.page_queue.get()            #取出url
            resp=requests.get(page_url,headers=headers)   #取出url发送请求
            json_data=resp.json()
            d = {}
            data_lst = json_data['List']
            for data in data_lst:
                image_url_lst = exact_url(data)
                sProdName = parse.unquote(data['sProdName'])
                d[sProdName] = image_url_lst
            for key in d:  # 拼接路径
                dirpath = os.path.join('image', key.strip(' ').replace('·','').replace('1:1',''))  #存储图片的路径
                if not os.path.exists(dirpath):
                    os.mkdir(dirpath)
                # 下载图片并进行保存
                for index,image_url in enumerate(d[key]):
                    #生产图片的url
                    self.image_url_queue.put({'image_page':os.path.join(dirpath,'{}.jpg'.format(index+1)),'image_url':image_url})
                           #此处不用下载,下载交给消费者线程


#消费者线程
class Customer(threading.Thread):
    def __init__(self,image_url_queue):           #定义消费者对象中的变量
        super().__init__()
        self.image_url_queue = image_url_queue
    def run(self):
        while True:
            try:
                image_obj=self.image_url_queue.get(timeout=20)
                request.urlretrieve(image_obj['image_url'], image_obj['image_page'])
                print(f'{image_obj["image_page"]}下载完成')
            except:
                break


#定义一个启动线程的函数
def start():
    page_queue= Queue(22)    #存储每一页面的url
    image_url_queue= Queue(1000)  #存储图片的路径
    for i in range(0,22):
        page_url=f'https://apps.game.qq.com/cgi-bin/ams/module/ishow/V1.0/query/workList_inc.cgi?activityId=2735&sVerifyCode=ABCD&sDataType=JSON&iListNum=20&totalpage=0&page={i}&iOrder=0&iSortNumClose=1&_everyRead=true&iTypeId=2&iFlowId=267733&iActId=2735&iModuleId=2735&_=1670142334580'
        #print(page_url)
        page_queue.put(page_url)
    #创建生产者对象
    for i in range(10):
        t=Producer(page_queue,image_url_queue)
        t.start()


    #创建消费者对象
    for i in range(20):
        t = Customer(image_url_queue)
        t.start()

if __name__ == '__main__':
    start()

本文为本人学习资料,仅供学习参考,如有侵权行为请联系作者删除

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值