爬虫--05:多线程

多线程

一、多线程的简介(基本介绍)

  • 有很多的场景中的事情是同时进行的,比如开车的时候 手和脚共同来驾驶汽车,再比如唱歌跳舞也是同时进行的
  • 我们在Python中有一个内置模块可以专门来实现多线程 threading
  • 程序中模拟多任务
import time

def sing():
    for i in range(3):
        print("正在唱歌...%d"%i)
        time.sleep(1)

def dance():
    for i in range(3):
        print("正在跳舞...%d"%i)
        time.sleep(1)

if __name__ == '__main__':
    sing()
    dance()

二、创建多线程

1、通过函数创建多线程

  • 使用threading模块中的Thread类,有一个target参数,这个参数需要我们传递一个函数对象 这个函数可以实现子线程的逻辑
import threading
import time
​
def demo():
    # 子线程
    print('hello 子线程')if __name__ == '__main__':
    for i in range(5):
        t = threading.Thread(target=demo)
        time.sleep(1)
        t.start() # 开始并启动线程

2、通过类创建多线程

  • 自定义一个类,需要继承父类 threading.Thread 并覆盖run()方法 在run()方法中实现子线程的逻辑
class A(threading.Thread):def run(self) -> None:
        for i in range(5):
            print(i)if __name__ == '__main__':
​
    a = A()
    a.start()

三、查看线程的数量

  • threading.enumerate() 查看当前线程的数量

1、主线程与子线程的执行关系

  • 主线程会等待子线程执行结束之后,主线程执行结束
  • join()等待子线程结束之后,主线程继续执行
  • setDaemon()守护线程,不会等待子线程结束
我们可以通过一个方法 threading里面的方法 enumerate()
def demo1():
    for i in range(5):
        print('demo1--%d'%i)
        time.sleep(1)def demo2():
    for i in range(10):
        print('demo2--%d' % i)
        time.sleep(1)def main():
    t1 = threading.Thread(target=demo1)
    t2 = threading.Thread(target=demo2)
    t1.start()
    t2.start()
    while True:
        print(threading.enumerate())
        if len(threading.enumerate()) <= 1:
            break
        time.sleep(1)if __name__ == '__main__':
    main()

2、子线程的执行与创建

  • 通过函数创建子线程
threading
import time


def dome():
    for i in range(5):
        print('demo1--{}'.format(i))
        time.sleep(1)


def main():
    print(threading.enumerate())
    t = threading.Thread(target=dome) # 如果你创建了子线程
    print(threading.enumerate()) # 结果是1还是2
    t.start() # 如果是你创建了子线程    它创建了子线程
    print(threading.enumerate())


if __name__ == '__main__':
    main()
  • 通过类创建子线程
class A(threading.Thread):

    def ran(self) -> None:  # 意思是默认返回值是None
        for i in range(5):
            print(i)


if __name__ == '__main__':
    a = A()
    a.ran()
    a.start()

四、线程之间的资源竞争

  • 一个线程写入,一个线程读取,没问题,如果两个线程都写入呢?
  • 解决办法是线程之间共享全部变量
    函数:
import threading
import time
num = 0


def demo1(nums):
    global num
    for i in range(nums):
        num += 1
    print('demo1--num-{}'.format(num))


def demo2(nums):
    global num
    for i in range(nums):
        num += 1
    print('demo2--num-{}'.format(num))


def main():
    t1 = threading.Thread(target=demo1, args=(100000000,))
    t2 = threading.Thread(target=demo2, args=(100000000,))
    t1.start()
    time.sleep(3)
    t2.start()
    time.sleep(3)
    print('main--num-{}'.format(num))


if __name__ == '__main__':
	main()

五、线程锁(互斥锁与死锁)

1、互斥锁

  • 设置线程锁lock = threading.Lock()
  • 上锁:lock.acquire()
  • 解锁:lock.release()
import threading
import time

# 创建一个线程锁
lock = threading.Lock()


num = 0
def demo1(nums):
    global num
    # 上线程锁
    # 这种创建锁的方式叫做不可重复的锁,只能创建一次
    lock.acquire()
    for i in range(nums):
        num += 1
    # 解锁
    lock.release()
    print('demo1--num-{}'.format(num))


def demo2(nums):
    global num
    lock.acquire()
    for i in range(nums):
        num += 1
    lock.release()
    print('demo2--num-{}'.format(num))


def main():
    t1 = threading.Thread(target=demo1, args=(1000000,))
    t2 = threading.Thread(target=demo2, args=(1000000,))
    t1.start()
    t2.start()
    time.sleep(3)
    print('main--num-{}'.format(num))


if __name__ == '__main__':
    main()

2、死锁

  • 在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        # 对mutexA上锁
        mutexA.acquire()

        # mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
        print(self.name+'----do1---up----')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
        mutexB.acquire()
        print(self.name+'----do1---down----')
        mutexB.release()

        # 对mutexA解锁
        mutexA.release()

class MyThread2(threading.Thread):
    def run(self):
        # 对mutexB上锁
        mutexB.acquire()

        # mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
        print(self.name+'----do2---up----')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
        mutexA.acquire()
        print(self.name+'----do2---down----')
        mutexA.release()

        # 对mutexB解锁
        mutexB.release()

mutexA = threading.Lock()
mutexB = threading.Lock()

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

3、避免死锁

  • 程序设计时要尽量避免
  • 添加超时等待

生产者与消费者模型

一、Queue线程(队列)

  • 在线程中,访问一些全局变量,加锁是一个经常的过程。如果你是想把一些数据存储到某个队列中,那么Python内置了一个线程安全的模块叫做queue模块。
  • Python中的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列Queue,LIFO(后入先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。
初始化Queue(maxsize):创建一个先进先出的队列。
empty():判断队列是否为空。
full():判断队列是否满了。
get():从队列中取最后一个数据。
put():将一个数据放到队列中。
  • 队列的快速入门
from queue import Queue


'''
empty() :判断队列是否为空
full() :判断队列是否慢了
get() : 从队列中获取一个数据
put() :将一个数据放在队列中
'''
q = Queue(2) # 指定有几个队列
# print(q.empty()) # 返回Turn,表示现在队列为空,False队列不为空
# print(q.full()) # False表示队列不是满的,Turn表示队列是满的

q.put(1)
q.put(2)
print('-'*50)
print(q.full())
print(q.empty())
q.put(3, timeout=3) # 程序堵塞解决方法,查看源代码
q.put_nowait(5)
print(q.get())
print(q.get())
print(q.get_nowait())

二、生产者和消费者

  • 生产者和消费者模式是多线程开发中常见的一种模式。通过生产者和消费者模式,可以让代码达到高内聚低耦合的目标,线程管理更加方便,程序分工更加明确。
  • 生产者的线程专门用来生产一些数据,然后存放到容器中(中间变量)。消费者在从这个中间的容器中取出数据进行消费
    在这里插入图片描述

1、Lock版的生产者和消费者

import threading
import random
gMoney = 0
# 定义一个变脸,可以保存生产的次数,默认是0次。
gTimes = 0
gLock = threading.Lock() # 引入线程锁


# 定义生产者
class Producer(threading.Thread):
    # 生产者逻辑
    def run(self) ->None:
        global gMoney
        global gTimes
        while True:
            gLock.acquire() # 上锁
            if gTimes >= 10:
                gLock.release() # 在满足十次生产之后进行解锁
                break
            money = random.randint(0, 100) # 0 <= money <= 100
            gMoney += money # 开始赚钱
            gTimes += 1
            print('%s生产了%d元钱' % (threading.current_thread().name, money))
            gLock.release() # 解锁


# 定义消费者
class Consumer(threading.Thread):
    # 消费者逻辑
    def run(self) ->None:
        global gMoney
        while True:
            gLock.acquire()  # 上锁
            money = random.randint(0, 100)  # 0 <= money <= 100
            if gMoney > money:
                gMoney -= money # 开始花钱
                print('%s消费了%d元钱' % (threading.current_thread().name, money))
            else:
                if gTimes >= 10:
                    gLock.release()
                    break
                print('%s想消费%d元钱,但是余额只有%d'% (threading.current_thread().name, money, gMoney))
            gLock.release()  # 解锁


def main():
    # 开启五个生产者线程
    for x in range(5):
        th = Producer(name='生产者{}号'.format(x))
        th.start()
    # 开启五个消费者模型
    for i in range(5):
        ph = Consumer(name='消费者{}号'.format(i))
        ph.start()


if __name__ == '__main__':
    main()

2、Condition版的生产者和消费者

import threading
import random
gMoney = 0
# 定义一个变量 保存生产的次数 默认是0次
gTimes = 0
# 定义一把锁
# gLock = threading.Lock()
gCond = threading.Condition()
# 定义生产者
class Producer(threading.Thread):

    def run(self):
        global gMoney
        global gTimes

        while True:
            gCond.acquire() # 上锁
            if gTimes >= 10:
                gCond.release()
                break
            money = random.randint(0,100)
            gMoney += money
            gTimes += 1
            print("%s生产了%d元钱,剩余%d元钱" % (threading.current_thread().name, money, gMoney))
            gCond.notify_all()
            gCond.release() # 解锁
# 定义消费者
class Consumer(threading.Thread):

    def run(self):
        global gMoney
        while True:
            gCond.acquire()  # 上锁
            money = random.randint(0, 100)

            while gMoney < money:
                if gTimes >= 10:
                    gCond.release()
                    return  # 这里如果用break只能退出外层循环,所以我们直接return
                print("%s想消费%d元钱,但是余额只有%d元钱了,并且生产者已经不再生产了!"%(threading.current_thread().name,money,gMoney))
                gCond.wait()
            # 开始消费
            gMoney -= money
            print("%s消费了%d元钱,剩余%d元钱" % (threading.current_thread().name, money, gMoney))
            gCond.release()

def main():
    # 开启5个生产者线程
    for x in range(5):
        th = Producer(name="生产者%d号" % x)
        th.start()
    # 开启5个消费者线程
    for x in range(5):
        th = Consumer(name="消费者%d号" % x)
        th.start()

if __name__ == '__main__':
    main()

案例演示

一、普通方式爬取王者荣耀皮肤图片

思路:
1、第一步:找到真正的数据接口,删除jsoncallback=jquery得到相应结果
2、第二步:解析响应结果 得到不同规格的图片url
3、第三步:保存数据 \image\兰陵王-默契交锋3
问题分析:
1、我们发现数据不在网页的源码当中,然后通过network找到了它的真正的数据接口 worklist
2、response数据复制到json.cn的网站 发现并没有解析出数据来
需要我们jsoncallback=jquery的数据删掉 发现数据正常了
3、每一个object里面就是一组图片 ProdImgNo_1是封面小图 其它的规格图片分别是 2 3 4 5 6 7 8
发现图片的url做了编码了 parse.unquote进行一个解码
需要把图片的后缀 200–> 0

import json
response = requests.get(url)
data1 = json.loads(response.text) # python类型的dict

data2 = response.json() # python类型的dict
response.json() 是requests的一种响应方式
json.loads(response.text) 是python内置的一个json模块得到的一种方式 跟requests库没有关系
​
import os
dirpath = os.path.join('image',name)
os.mkdir(dirpath)

代码演示:

# 普通方式爬取王者荣耀图片
# https://apps.game.qq.com/cgi-bin/ams/module/ishow/V1.0/query/workList_inc.cgi?activityId=2735&sVerifyCode=ABCD&sDataType=JSON&iListNum=4&totalpage=0&page=0&iOrder=0&iSortNumClose=1&iAMSActivityId=51991&_everyRead=true&iTypeId=1&iFlowId=267733&iActId=2735&iModuleId=2735&_=1616562654156
# jsoncallback= 返回的是jQuery。


import requests
import os
from urllib import parse
from urllib import request


headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36 Edg/89.0.774.57',
'referer':'https://pvp.qq.com/'
}
# 定义一个函数:用来获取并解析不同规格图片的url
def extract_images(data):
    image_urls = []
    for x in range(1, 9):
        image_url = parse.unquote(data['sProdImgNo_{}'.format(x)]).replace('200', '0')
        image_urls.append(image_url)

    return image_urls


def main():
    # 目标url
    page_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=0&iOrder=0&iSortNumClose=1&iAMSActivityId=51991&_everyRead=true&iTypeId=2&iFlowId=267733&iActId=2735&iModuleId=2735&_=1616245467736'

    res = requests.get(page_url, headers=headers)
    # print(res.text) # 得到json类型的字符串
    result = res.json() # 将json类型转换成python类型
    datas = result['List']
    for data in datas:
        # 获取图片的url
        # image_urls = extract_images(data)--意思是将以上data参数传递给extract_images()函数,再将extract_images()函数所得到的值传递给image_urls(这个值是一个列表)
        image_urls = extract_images(data)

        # 获取图片的名字
        name = parse.unquote(data['sProdName'])
        # 路径 \image\兰陵王-默契交锋3\1.jpg 2.jpg 3.jpg。。。。
        dirpath = os.path.join('image', name) # 构建文件夹路径
        os.mkdir(dirpath) # 创建一个文件夹

        # 保存图片
        for index, image_url in enumerate(image_urls):
            request.urlretrieve(image_url, os.path.join(dirpath, '{}.jpg'.format(index+1))) # request.urlretrieve需要传递参数(参数是需要下载的东西的地址和下载的路径)
            print('{}下载完成'.format(name))


if __name__ == '__main__':
    main()

二、通过多线程的方式爬取王者荣耀皮肤图片

1 生产者和消费者模型来解决的
不推荐上锁 整个的流程需要清晰的明确

2 os 模块的引用
os.mkdir()创建文件夹
os.path.join 动态的指定路径
3 考虑到文件的命名问题 enumerate() 这个方法来解决图片的名字问题
for index,image_url in enumerate(image_urls): # 0 - 7
request.urlretrieve(image_url,os.path.join(dirpath,’%d.jpg’%(index+1)))

4 翻页 page=0
for x in range(0,3):
url = ‘totalpage=0&page={}’.format(x)
print(url)

for x in range(0,3):
url = ‘totalpage=0&page={page}’.format(page = x)
print(url)

4 创建队列 一个是保存目的url的队列 一个是保存图片url和路径的队列

5 生产者
q.empty()True 队列是空的 False有数据 not False - True
while not self.page_queue.empty():

6 消费者
有可能生产者还没有生成数据 然后我就去get()数据 while True get(timeout=10) 出现异常我们通过try语句来解决异常

7 super()继承的使用

代码演示:

import os # 创建文件路径模块
import queue # 队列模块
import requests # 请求模块
import threading # 多线程模块
from urllib import request # 请求并保存模块
from urllib import parse # 16进制翻译模块


# 核心思路是通过生产者消费者模型来编写
headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_3_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36'
}

# 定义生产者
class Producer(threading.Thread):

    def __init__(self, page_queue, image_queue, *aras, **kwargs):
        # 继承方
        super(Producer, self).__init__(*aras, **kwargs)

        self.page_queue = page_queue
        self.image_queue = image_queue

    def run(self) -> None:  # q.qmpty() 队列是空的返回的是Ture
        while not self.page_queue.empty():  # 如果这个队列不为空,则向下执行
            page_url = self.page_queue.get()  # 将目标的url存放在队列一中
            res = requests.get(page_url, headers=headers)

            # print(res.text) # 得到json类型的字符串
            result = res.json()  # 将json类型转换成python类型
            datas = result['List']
            for data in datas:
                # 获取图片的url
                # image_urls = extract_images(data)--意思是将以上data参数传递给extract_images()函数,再将extract_images()函数所得到的值传递给image_urls(这个值是一个列表)
                image_urls = extract_images(data)

                # 获取图片的名字
                name = parse.unquote(data['sProdName'])
                # 路径 \image\兰陵王-默契交锋3\1.jpg 2.jpg 3.jpg。。。。
                dirpath = os.path.join('image', name)  # 构建文件夹路径
                if not os.path.exists(dirpath):  # 如果没有文件夹,采取创建image文件夹
                    os.mkdir(dirpath)  # 创建一个文件夹

                # 将图片的url存放到队列2中
                for index, image_url in enumerate(image_urls):
                    # 将图片的下载地址和下载路径放在一个队列二的字典中
                    self.image_queue.put({'image_url': image_url,
                                          'image_path': os.path.join(dirpath, '{}.jpg'.format(index + 1))})

# 定义消费者
class Consumer(threading.Thread):
    def __init__(self, image_queue, *aras, **kwargs):
        # 通过super来调用父类中的init,即子类需要实现自己的init方法就需要调用父类的init方法
        super(Consumer, self).__init__(*aras, **kwargs)
        self.image_queue = image_queue

    def run(self) -> None:
        while True:
            try:
                # 取出队列二中存放图片下载地址和下载路径的字典
                image_obj = self.image_queue.get(timeout=10)
                # 分别获取图片的url和下载图片的路径
                image_url = image_obj.get('image_url')  # 图片的下载地址
                image_path = image_obj.get('image_path')  # 图片的下载路径

                # 下载图片
                try:
                    request.urlretrieve(image_url, image_path)
                    print(image_path, '下载完成!')
                except:
                    print(image_path, '下载失败!')
            except:
                break

def extract_images(data):
    image_urls = []
    for x in range(1, 9):
        image_url = parse.unquote(data['sProdImgNo_{}'.format(x)]).replace('200',
                                                                           '0')  # replace()方法的意思是将"200"替换为"0"
        image_urls.append(image_url)

    return image_urls

def main():

    # 队列一:创建页数的队列
    page_queue = queue.Queue(10)
    # 队列二:创建图片队列
    image_queue = queue.Queue(1000)

    # 明确目标的url
    for x in range(0, 10):
        page_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={page}&iOrder=0&iSortNumClose=1&iAMSActivityId=51991&_everyRead=true&iTypeId=2&iFlowId=267733&iActId=2735&iModuleId=2735&_=1616245467736'.format(
            page=x)
        # 将页数的url添加到队列当中
        page_queue.put(page_url)

    # 定义24个生产者线程
    for x in range(10):
        th = Producer(page_queue, image_queue)
        th.start()
    # 定义25个消费者线程
    for x in range(25):
        th = Consumer(image_queue)
        th.start()

if __name__ == '__main__':
    main()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值