目录
一、背景
1.1、前言
1.2、说明
二、线程与进程
2.1、什么是进程
2.2、什么是线程
2.3、进程与线程的关系
2.4、多进程与多线程的最佳使用条件
2.5、线程与进程的锁
2.6、特别注意
三、第一个线程、线程池
3.1、线程测试
3.2、执行结果
3.3、线程池测试
3.4、执行结果
四、线程池批量下载美妞图片
4.1、步骤
4.2、完整代码
4.3、结果
五、一些线程函数
5.1、start()函数
5.2、join()函数
5.3、setDaemon()函数
5.4、Lock()函数、RLock()函数、acquire()函数与release()函数
六、总结
一、背景
1.1、前言
在爬虫获取资源的过程中,有时需要批量下载资源,但一次次的发起请求在下载速度实在太慢,需要提高获取资源的速度。那么线程与进程的知识就必不可少,下面对进程与线程进行学习并结合实战提高知识掌握程度。
1.2、说明
操作系统:win 10
编辑器:pycharm edu
语言及版本:python 3.10
使用的库:ThreadPoolExecutor、requests、BeautifulSoup
实现思路:写出单线程的代码,在把任务提交到线程池或者进程池即可
说明:下文不使用类的继承方法重写run函数,进行线程的运行
tips:以下代码url不会放真实的,移植测试注意识别更改
二、线程与进程
2.1、什么是进程
进程(运行中的程序)。每次执行一个程序,操作系统自动为该程序分配资源(如内存分配,创建一个可执行线程)。
2.2、什么是线程
线程(在运行中的程序内),是进程中的实际运作单位。可以直接被CPU调度的执行过程,是操作系统中可以进行运算调度的最小单位。
注意:不同进程之间的内存,默认不允许其它进程进行使用;
2.3、进程与线程的关系
进程是资源单位,线程是执行这些资源的运行单位。
小例子:进程是银行开设的某个业务,线程是办理这些业务的具体窗口。
关键词:资源单位、执行单位
2.4、多进程与多线程的最佳使用条件
1)什么时候使用多进程比较好?
计算密集型,使用多进程,如:大量的数据计算
各个部分相互独立,很少产生交集;
例子:代理池
1)获取到IP
2)验证IP是否可用
3)对外提供接口
以上三个步骤,相互独立,几乎不产生交集,使用多进程的效果更好
2)什么时候使用多线程比较好
I/O密集型,使用多线程,如:文件读写、网络数据传输(下载视频);
各个部分完成过程相似;
例子:批量获取图片、音乐、菜价等
1)获取内容url
2)发起请求
3)下载保存
以上的获取图片,音乐等步骤都是一致的,都是获取到该内容的url,然后对url发起请求,在下载保存,高度相似,使用多线程的效果更好。
2.5、线程与进程的锁
1)GIL锁
全局解释锁,是CPython解释器特有的,让一个进程中同一时刻只能有一个线程可以被CPU调用。(注意,此时如果有多个线程,那么CPU会轮流切换到不同的线程进行运行(不管当前线程是否结束),保证了线程的唯一运行性);
2)线程安全
单依靠线程的GIL锁数据是不安全的,所以要为线程申请锁,存在以下两种情况:
情况一:当多个线程同时申请一把锁时,第一个申请到锁的线程可以完整执行当前线程的程序,在释放所后;第二个申请到锁的线程继续执行当前线程的程序(此时其它没申请到锁的线程是阻塞状态);
情况二:多个线程申请不同的锁时,第一个申请到锁的线程开始执行,其它线程是阻塞状态,然后轮到第二个申请到锁的线程进行执行,直到全部执行结束;
3)死锁
出现原因:由于线程间竞争资源或者由于彼此通信而造成的一种阻塞现象;
结果:此时程序会卡着不动,一动不动;
例子一:模块1给申请了锁1,模块1申请了锁2,过了一会,模块1又跑去申请锁2(此时锁1、2都没有释放),或者模块2又跑去申请锁1,此时就会出现死锁。
2.6、特别注意
使用多线程、多进程、携程、进程池、线程池等方法提高程序运行速度时,需要考虑访问网站的承受能力,避免高速访问造成攻击。
三、第一个线程、线程池
3.1、线程测试
from threading import Thread # 导入线程库
def function1(name): # 函数1
for i in range(1, 500):
print(name, i)
def function2(name): # 函数2
for j in range(1, 500):
print(name, j)
if __name__ == '__main__':
# 创建线程并设置参数,target是执行的函数,args是传参并且传递是参数规定是元组,
t1 = Thread(target=function1, args=("海绵宝宝",))
t2 = Thread(target=function2, args=("派大星",))
t1.start() # 开启线程
t2.start()
for k in range(1, 500):
print("我们去抓水母吧", k)
3.2、执行结果
可以看到多个函数并发运行输出结果,说明此方法可行。
如下图1:
图1
3.3、线程池测试
from concurrent.futures import ThreadPoolExecutor # 线程池库
def function(name): # 测试函数
print("我是", name)
return name
def get_return(res): # 拿返回结果函数
print(res.result())
if __name__ == '__main__':
with ThreadPoolExecutor(3) as t: # 分配三个线程池,并且命名为t
# 线程池t提交,添加返回绑定函数
t.submit(function, "海绵宝宝").add_done_callback(get_return)
t.submit(function, "派大星").add_done_callback(get_return)
t.submit(function, "章鱼哥").add_done_callback(get_return)
3.4、执行结果
如下图2:
图2
说明:由于进程与进程池的代码与线程的高度相似,只是导入的包不同,所以对该部分内容进行了跳过;
四、线程池批量下载美妞图片
4.1、步骤
1)请求到合集url
2)拿到每一张图片跳转url
3)拿到每一张图片下载地址
4)线程池执行
4.2、完整代码
代码需要注意的是:我们请求的url只有4个,线程池里的线程数量最好不要大于4,因为只有4个任务,你给多了线程也没啥用。
"""
批量下载美妞图片实现思路
1、
"""
from concurrent.futures import ThreadPoolExecutor
import requests
from bs4 import BeautifulSoup
def get_img_url():
# all_url = "https://www.yeitu.com/meinv/siwameitui/2.html"
for x in range(2, 6):
url = "https://www.yeitu.com/meinv/siwameitui/{}.html".format(x)
resp = requests.get(url, headers=header)
resp.encoding = "utf-8"
# print(resp.status_code)
result = BeautifulSoup(resp.text, "html.parser")
li_list = result.find("div", class_="list-box-p").find("ul").find_all("li")
o = 0
for i in li_list:
src = i.find("a").get("href")
two_resp = requests.get(src, headers=header)
# print(two_resp.status_code)
two_resp.encoding = "utf-8"
two_result = BeautifulSoup(two_resp.text, "html.parser")
img_list = two_result.find("div", class_="img_box").find("a")
for j in img_list:
o += 1
all_url = j.get("src")
three_resp = requests.get(all_url, headers=header)
name = all_url.split("/")[-1]
# print(three_resp.status_code)
img_name = "{}".format(name)
with open("tupian/"+img_name, mode="wb") as f:
f.write(three_resp.content)
print("图片{}下载完成".format(name))
if __name__ == '__main__':
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0'
}
with ThreadPoolExecutor(3) as t:
t.submit(get_img_url)
4.3、结果
可以看到下载的图片整整齐齐的保存到了文件中,并且下载速度很快,这就是线程池的威力。
五、一些线程函数
5.1、start()函数
用法:线程对象.start()
含义:表示子线程已经准备好了,CPU可以开始调度该子线程
5.2、join()函数
用法:线程对象.join()
含义:等待该线程执行结束后,程序才往后继续执行
5.3、setDaemon()函数
用法:线程对象.setDaemon(布尔值),守护线程(必须放置start之前)
含义:布尔值为True,则设置为守护线程,主线程执行完毕后,子线程也自动关闭;
布尔值为False,则设置为非守护线程,主线程等待子线程,子线程执行完毕后,主线程才结束;
注意:非守护线程是默认值
5.4、Lock()函数、RLock()函数、acquire()函数与release()函数
用法:线程对象.Lock()、线程对象.RLock()、线程对象.acquire()、线程对象.release()
含义:创建同步锁、创建递归锁、申请锁、释放锁
当线程申请到了锁时,其它线程就会处于阻塞状态,等当前线程执行结束并释放锁后,由第二个申请到锁的线程执行,如此反复;
说明:RLock可以多次申请锁和多次释放、Lock不支持
六、总结
难点:
1)概念多;
2)方法多;
优点:
1)快速提高程序运行速度;
2)代码上手速度快;
总结:
1)多学习。