Crawler - 05: Multithreading
多线程
一、多线程的简介(基本介绍)
- 有很多的场景中的事情是同时进行的,比如开车的时候 手和脚共同来驾驶汽车,再比如唱歌跳舞也是同时进行的
- 我们在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()