4.4 爬虫实例03 (多线程) 2021-06-18

前面两章给大家讲了爬虫基础和保存数据,这章来了解一下多线程。

什么是多线程?

多线程类似于同时执行多个不同程序,比如 吃饭是一个程序,看电视是一个程序,聊天也是一个程序,小王可以 在吃饭的时候看电视,同时还可以聊天。这个就是多线程,同时做很多事。

程序的运行取决于cpu的执行,我们采用多线程的目, 是为了提高cpu的利用率,加快程序的运行速度。

在Python3 线程中常用的两个模块为:
_thread
threading(推荐使用)

接下来我们看一个多线程的例子:

# 多线程爬虫

# 导入时间库
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,通过这个例子我们明白了多线程的有点,实际上我们在爬取的过程,可以总结为这几步:

  1. 获取url (核心:依赖于网络请求)
  2. 根据url获取网页内容 (核心:依赖于网络)
  3. 根据网页内容分析提取有效数据 (依赖于cpu的处理)
  4. 保存数据 (核心:将内容保存到磁盘)

实际上可以说是依赖于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之扑克牌游戏

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值