前面两章给大家讲了爬虫基础和保存数据,这章来了解一下多线程。
什么是多线程?
多线程类似于同时执行多个不同程序,比如 吃饭是一个程序,看电视是一个程序,聊天也是一个程序,小王可以 在吃饭的时候看电视,同时还可以聊天。这个就是多线程,同时做很多事。
程序的运行取决于cpu的执行,我们采用多线程的目, 是为了提高cpu的利用率,加快程序的运行速度。
在Python3 线程中常用的两个模块为:
_threadthreading
(推荐使用)
接下来我们看一个多线程的例子:
# 多线程爬虫
# 导入时间库
import time
#导线程库
import threading
# 一个打印时间的函数
def print_time(i: int):
print('hello---', i)
time.sleep(3) # 让程序暂停3秒,
# 单线程函数
def line_run():
for i in range(10):
print_time(i) #调用打印时间函数
# 多线程
def anync_run():
for i in range(10):
# 实例化一个新线程
'''
t = Thread(target=function_name, args=(function_parameter1, function_parameterN))
function_name: 需要线程去执行的方法名
args: 线程执行方法接收的参数,该属性是默认是一个元组,
# target
'''
thread = threading.Thread(target=print_time, args=[i])
# 启动线程
thread.start()
#调用多线程
anync_run()
# 调用单线程
line_run()
通过对比我们可以看出,单线程执行是暂停了的,总共用时30.01115655899048
多线程执行很快 用时 0.001963376998901367,大大提升了效率
多线程爬虫
那我们修改一下之前的爬虫代码,更能清晰的看出时间效率提升:
# 导入网络请求包
import requests
# 导入文本解析包
from bs4 import BeautifulSoup
import lxml
# 导入正则
import re
# 导入数学计算
import math
# 导入时间
import time
# 导入多线程包
import threading
# 设置基本的地址变量
base_url = 'https://www.ilync.cn/org/6818_d_0_0_-1_-1_0_'
http_url = 'https://www.ilync.cn/'
# 发起请求,获取网站内容
def get_content(url):
# header 模拟请求头 Referer 来源
header = {
'Referer': "https://www.ilync.cn/",
'Host': 'www.ilync.cn',
'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"
}
response = requests.get(url, headers=header)
response_text = response.content.decode("utf-8") # 返回文本进行utf-8编码
return response_text
# 获取总页码
def get_total_page(begin_page: int):
content = get_content(base_url + str(begin_page))
soup = BeautifulSoup(content, 'lxml') # 实例化一个对象
# 获取页面一个id是countCourseNum的input元素,代表总共多少数据
total = soup.find('input', id='countCourseNum').attrs['value']
# 一页24条数据,计算总共有多少页,使用math.ceil向上取整
return math.ceil(int(total) / 24)
"""根据文本分析内容"""
def get_soure(url: str):
content = get_content(url) # 调用get_content函数,获取文本
soup = BeautifulSoup(content, 'lxml') # 实例化一个对象
# 第一次进行筛选
first_filter = soup.find_all('div', class_='course-list-wrap')[0]
# 第二次筛选
second_filter = first_filter.find_all('div', class_='grid-cell')
# 循环获取所需的图片、标题、价格、id,课程
infos = []
for one in second_filter:
temp_dict = {}
img = one.find('img').attrs['src']
title = one.find('img').attrs['title']
price = one.find('div', class_='course-price').text
price = re.sub(r'\s', '', price) # 去除制表符、换行符
id_str = one.find('a', class_='course-pic').attrs['href']
id = id_str[id_str.find('_') + 1:id_str.find('?')]
temp_dict['id'] = id
temp_dict['title'] = re.sub(r'\xa0', ' ', title) # 空格转换
temp_dict['img'] = img
temp_dict['price'] = price
temp_dict['url'] = http_url + id_str
infos.append(temp_dict)
print(temp_dict,'\n', '=' * 10)
# 这里直接打印课程,更清晰看出数据
# 生成所有url元组,并返回
def get_url_list(page):
url = []
for i in range(1, page + 1):
url.append(base_url + str(i))
return url
"""单线程获取课程信息"""
def get_all_course_line(url_list: list):
for url in url_list:
# 根据url爬取这一页的数据
get_soure(url)
"""多线程获取课程信息"""
def get_all_course_thread(url_list:list):
while len(urls)>0:
url = urls.pop() # 获得列表中最后一个地址
# 实例化一个线程,调用get_soure方法,传参数是最后一个地址url
th = threading.Thread(target=get_soure, args=[url])
th.start() # 开始线程
"""
一段程序作为主线运行程序时其内置名称就是 __main__,
自己的 __name__ 在自己用时就是 main,当自己作为模块被调用时就是自己的名字,就相当于:我管自己叫我自己,但是在朋友眼里我就是小仙女(文件名)一样
"""
if __name__ == '__main__':
# 记录开始是的时间
start_time = time.time()
# 获取总页码数
total_page = get_total_page(1)
# 获取需要请求的url列表
urls = get_url_list(total_page)
'''
单线程请求
# get_all_course_line(urls)
'''
# 多线程请求,由于上面使用的是pop方法,这里对urls进行反序排列,
get_all_course_thread(urls.reverse())
# 记录结束时间
end_time = time.time()
# 查看时间差
print(end_time - start_time)
# 单线程时间差 5.978450536727905
# 多线程时间差 2.0488483905792236
两次的打印结果更明显的看出时间相差了近3s,通过这个例子我们明白了多线程的有点,实际上我们在爬取的过程,可以总结为这几步:
- 获取url (核心:依赖于网络请求)
- 根据url获取网页内容 (核心:依赖于网络)
- 根据网页内容分析提取有效数据 (依赖于cpu的处理)
- 保存数据 (核心:将内容保存到磁盘)
实际上可以说是依赖于cpu和网络,对于提升cpu的运行效率,这就是我们上面所运用的多线程了。
队列
这里我们要引入一个队列
的概念:就像是排队,一个个进行
Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue。
简单介绍一下Queue模块中的常用方法,
Queue.qsize() 返回队列的大小
Queue.empty() 如果队列为空,返回True,反之False
Queue.full() 如果队列满了,返回True,反之False
Queue.full 与 maxsize 大小对应
Queue.get([block[, timeout]])获取队列,timeout等待时间
Queue.get_nowait() 相当Queue.get(False)
Queue.put(item) 写入队列,timeout等待时间
Queue.put_nowait(item) 相当Queue.put(item, False)
Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
Queue.join() 实际上意味着等到队列为空,再执行别的操作
仍以我们上面的爬虫网页为例:实际上有三个队列来进行:
那么我们结合上面的多线程+队列来改造我们的爬虫方法:
'''
多线程队列执行爬取网页内容,使用class类来定义
'''
# 导入网络请求包
import requests
# 导入文本解析包
from bs4 import BeautifulSoup
import lxml
# 导入正则
import re
# 导入数学计算
import math
# 导入时间
import time
# 导入多线程包
import threading
#导入队列
from queue import Queue
# 定义一个多线程爬取类
class ilyncSpider():
def __init__(self):
# 基础课程地址
self.base_url = 'https://www.ilync.cn/org/6818_d_0_0_-1_-1_0_{}'
# 课程详细地址
self.http_url = 'https://www.ilync.cn/{}'
# 课程总页码
self.pages_num = 2
# 准备网络请求的headers
self.header = {
'Referer': "https://www.ilync.cn/",
'Host': 'www.ilync.cn',
'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"
}
# 实例化一个url队列
self.url_queue = Queue()
# 实例化一个html文本队列
self.html_queue = Queue()
# 定义一个content处理之后的内容队列
self.content_queue = Queue()
"""获取url列表"""
def get_url_list(self):
# 获取url 放入队列
for i in range(1, self.pages_num + 1):
'''
# 把url添加到队列url_queue中
# 使用format方法拼接
'''
self.url_queue.put(self.base_url.format(i))
"""根据url解析页面内容"""
def parse_url(self):
while True: # 进行反复取,直到队列为空
# 在url队列中取出一个url,先进先出
url = self.url_queue.get()
# 获取当前url对应页面的文本
response = requests.get(url, headers=self.header)
# 把返回的html文本放入html_queue队列
self.html_queue.put(response.content.decode("utf-8"))
# task_done:把取出的url完成
self.url_queue.task_done() # 取出的url在url对列里删除
"""根据页面文本筛选出数据"""
def get_content_list(self):
while True: # 反复取
# 在html文本队列中取出一个文本,进行筛选
content = self.html_queue.get()
soup = BeautifulSoup(content, 'lxml') # 实例化一个对象
# 第一次进行筛选
first_filter = soup.find_all('div', class_='course-list-wrap')[0]
# 第二次进行筛选
second_filter = first_filter.find_all('div', class_='grid-cell')
# 循环获取所需的图片、标题、价格、id,课程
infos = []
for one in second_filter:
temp_dict = {}
img = one.find('img').attrs['src']
title = one.find('img').attrs['title']
price = one.find('div', class_='course-price').text
price = re.sub(r'\s', '', price) # 去除制表符、换行符
id_str = one.find('a', class_='course-pic').attrs['href']
id = id_str[id_str.find('_') + 1:id_str.find('?')]
temp_dict['id'] = id
temp_dict['title'] = re.sub(r'\xa0', ' ', title) # 空格转换
temp_dict['img'] = img
temp_dict['price'] = price
temp_dict['url'] = self.http_url.format(id_str)
infos.append(temp_dict)
# 添加数据到数据队列
self.content_queue.put(infos)
# 完成当前的进程
self.html_queue.task_done()
"""保存数据"""
def save_content_list(self):
while True: # 反复取
# 获取筛选的content队列中的内容
content_list = self.content_queue.get()
# 输出获得的内容
for i in content_list:
print(i)
# 结束
self.content_queue.task_done()
"""使用多线程调用"""
def run(self):
# 定义一个进程集合
thread_list = []
# 1 ======== 获取url
t_url = threading.Thread(target=self.get_url_list)
# 添加到集合中
thread_list.append(t_url)
# 2 ====== 多线程获取网页文本
for i in range(10):
t_xml = threading.Thread(target=self.parse_url)
thread_list.append(t_xml)
# 3 ====== 获取有效数据
t_content = threading.Thread(target=self.get_content_list)
thread_list.append(t_content)
# 4 ====== 保存数据
t_save = threading.Thread(target=self.save_content_list)
thread_list.append(t_save)
# 启动所有线程 固定模式
for t in thread_list:
# 启用守护进程
t.setDaemon(True)
# 启动
t.start()
# 队列中所有的都完成 程序结束
for q in [self.url_queue, self.html_queue, self.content_queue]:
q.join()
print('所有数据获取完成')
start = time.time()
# 实例化一个ilyncSpider对象:
obj = ilyncSpider()
# 调用run开始爬取数据
obj.run()
end = time.time()
print(end - start)
代码执行效率又提升了呢 ==
以上的代码是完整的,可以自己执行试试哟~
下一章python之扑克牌游戏