关于python多线程以及线程池的使用2 (ThreadPoolExecutor)

前一篇记录了python threading的学习及使用:
https://blog.csdn.net/qq_43906274/article/details/105716179

但听说实际开发应用中线程池以及异步I/O才是用得最多的,就这几天的学习做一下记录。
脑图如下:
在这里插入图片描述
Exectuor 提供了如下常用方法:
submit(fn, *args, **kwargs):将 fn 函数提交给线程池。*args 代表传给 fn 函数的参数,*kwargs 代表以关键字参数的形式为 fn 函数传入参数。submit()是非阻塞的,会立即返回一个Future对象

map(func, *iterables, timeout=None, chunksize=1):该函数类似于全局函数 map(func, *iterables),只是该函数将会启动多个线程,以异步方式立即对 iterables 执行map处理。map可以直接传入一个可迭代对象,map会遵循按传入顺序处理

shutdown(wait=True):关闭线程池。线程池实现了上下文管理协议(Context Manage Protocol),因此,程序可以使用 with 语句来管理线程池,使用with语法会显得更加简洁,且不必担心关闭线程池问题。

返回的Future 提供了如下方法:
cancel():取消该 Future 代表的线程任务。如果该任务正在执行,不可取消,则该方法返回 False;否则,程序会取消该任务,并返回 True。
cancelled():返回 Future 代表的线程任务是否被成功取消。
running():如果该 Future 代表的线程任务正在执行、不可被取消,该方法返回 True。
done():如果该 Funture 代表的线程任务被成功取消或执行完成,则该方法返回 True。
result(timeout=None):获取该 Future 代表的线程任务最后返回的结果。如果 Future 代表的线程任务还未完成,该方法将会阻塞当前线程,其中 timeout 参数指定最多阻塞多少秒。
exception(timeout=None):获取该 Future 代表的线程任务所引发的异常。如果该任务成功完成,没有异常,则该方法返回 None。
add_done_callback(fn):为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该 fn 函数。

1.基础理解示例:
对阻塞/非阻塞,最大线程数,执行顺序的理解有帮助

# -*- coding=utf-8 -*-
import time
from concurrent.futures.thread import ThreadPoolExecutor


def get_html(times):
    time.sleep(times)
    print("get html {} success".format(times))
    return times

with ThreadPoolExecutor(max_workers=2) as executor:
    # 试验1:单个任务提交
    # start_time = time.time()
    # task1= executor.submit(get_html,2)
    # print(time.time()-start_time)   #返回消耗时间:0.0010020732879638672,2秒后返回结果get html 2 success
    '''证明:submit()是非阻塞的,所以在主线程出现0秒的耗时。
    submit()提交任务后立刻返回结果,返回的结果为future对象,如果要取其结果,可以使用task.result(),但使用后会变成阻塞的,直到结果返回才会结束main函数'''

    # 试验2,多个任务提交
    # start_time = time.time()
    # task1= executor.submit(get_html,2)
    # task2 = executor.submit(get_html,3)
    # task3 = executor.submit(get_html,4)
    # print(task1.result())   #返回2
    # print(task2.result())   #返回3
    # print(task3.result())   #返回4
    # print(time.time()-start_time) #6.001647233963013
    '''证明:1.根据定义可以同时跑2个线程,2s和3s的同时执行,最终耗费时间为3s。
    task3需要等到task1的2s先跑完才能开始跑,而这是task2的3s还在跑,所以时间耗费2+4=6s'''

    # 试验3:多任务提交执行顺序
    start_time = time.time()
    # task = executor.submit(get_html,[2,3,5,1])  #函数参数值接收一个int型的,这里却给了list而报错
    task = [executor.submit(get_html,times) for times in [2,3,5,1]]
    print(type(task[0]))
    for future in task:
        print(future.result())
    print(time.time() - start_time)
    '''输出顺序:2,3,1,5,时间7s
    按照上面的理解2,3同时执行,2执行完了5进入,3执行完了1进入,
    1执行完了先返回了,5还没有执行完。所以1在5前面。'''

2.add_done_callback()和as_completed的使用

add_done_callback():
submit后返回的future对象可以调用add_done_callback()这个回调函数,()填入要进一步处理的方法名称,add_done_callback()会将future对象传入需要进一步处理的()方法内

as_complete():
as_compelete实际上是一个生成器,会把已经完成的任务yield成future对象,因此可以直接通过for 循环取出返回的结果
as_completed() 方法是一个生成器,在没有任务完成的时候,会一直阻塞,除非设置了 timeout。

# -*- coding=utf-8 -*-
import time
from concurrent.futures.thread import ThreadPoolExecutor
from concurrent.futures import as_completed, wait


def get_html(times):
    time.sleep(times)
    print("get html {} success".format(times))
    return times

def handle_html(future):
    time.sleep(future.result())
    print("handle html {} success".format(future.result()))
    return future.result()

with ThreadPoolExecutor(max_workers=2) as executor:
    # 试验:多任务提交后返回结果加入回调到handle_html
    # task = [executor.submit(get_html,times) for times in [2,3,5,1]]
    # # wait(task)
    # for future in task:
    #    future.add_done_callback(handle_html)
    # print("main")
    '''如果使用add_done_callback()函数:
    1.传入该函数的是上一个任务的future对象,需要在handle_html函数内部做取值或其他处理
    2.调用add_done_callback(),主线程依然是不阻塞的,如果不加wait(),main会先被打印出来'''

    #试验:试验as_completed
    task = [executor.submit(get_html, times) for times in [2, 3, 5, 1]]
    for future in as_completed(task):
        future.add_done_callback(handle_html)
    print("main")
    '''使用了as_completed后main会最后打出来,遵循谁先搞完就先处理谁'''

最后,对之前编写爬取51job的代码使用线程池进行改造
原代码在:
https://blog.csdn.net/qq_43906274/article/details/105590641

修改使用线程池:

# -*- coding=utf-8 -*-

import requests
from concurrent.futures import ThreadPoolExecutor, wait, as_completed
from threading import current_thread
from queue import Queue
from lxml import etree


def crawl_page(data_queue,url):
    # 默认请求头
    header = {
        "Accept":
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "Accept-Encoding": "gzip, deflate, br",
        "Accept-Language": "zh-CN,zh;q=0.9",
        "Connection": "keep-alive",
        "Host": "search.51job.com",
        "Upgrade-Insecure-Requests": "1",
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3610.2 Safari/537.36",
    }
    # print("当前工作线程为:{}".format(current_thread().name))
    response = requests.get(url=url, headers=header)
    print("当前爬取的url为:{}".format(url), "爬取页面响应状态码为:{}".format(response.status_code))
    response.encoding = 'gbk'
    data_queue.put(response.text)
    # print("当前data_queue的数据总量为:{}".format(data_queue.qsize()))
    return data_queue

def crawl_data(data_queue):
    # print("当前处理文本数据的线程为:{}".format(current_thread().name))
    # print("当前剩余数据量为{}".format(data_queue.result().qsize()))
    try:
        text = data_queue.result().get(block=False)
        html = etree.HTML(text)
        all_div = html.xpath("//div[@id='resultList']//div[@class='el']")
        info_list = []
        for item in all_div:
            info = {}
            # 获取数据的时候,使用列表索引为0的数据
            info['job_name'] = item.xpath("./p/span/a/@title")[0]
            info['company_name'] = item.xpath(".//span[@class='t2']/a/@title")[0]
            info['company_address'] = item.xpath(".//span[@class='t3']/text()")[0]
            # money字段可能为空,try-except来进行异常处理
            try:
                info['money'] = item.xpath(".//span[@class='t4']/text()")[0]
            except:
                info['money'] = '无数据'
            info['date'] = item.xpath(".//span[@class='t5']/text()")[0]
            info_list.append(info)
        print("解析出的数据为:{}".format(info_list))
    except:
        print("队列已满")
    return info_list



def main():
    # 构造存放页码和文本数据队列
    page_queue = Queue(maxsize=1000)
    data_queue = Queue(maxsize=1000)

    # 构造url,存入需要爬取的网页数据
    for page in range(1, 5):
        page_url = 'https://search.51job.com/list/000000,000000,0000,00,9,99,python,2,{}.html'.format(page)
        try:
            page_queue.put(page_url,block=False) #block不设置为False,队列满了就会一直停在那等待
        except:
            print("队列已满")

    print("当前队列中共有页码数为:{}".format(page_queue.qsize()))

    # 启动线程爬取页面信息
    with ThreadPoolExecutor(max_workers=2) as executor:  #使用with 语法在线程池使用完毕后可自动回收,否则需调用shutdown
        all_task = []
        for url in range(page_queue.qsize()):
            url = page_queue.get()
            task = executor.submit(crawl_page, *(data_queue, url))
            all_task.append(task)
        for future in as_completed(all_task):    # as_completed,哪个先完成就处理哪个,会阻塞主线程,知道完成所有,除非设置timeout
            future.add_done_callback(crawl_data)  #add_done_callback回调函数,功能是将future传入crawl_data

        # print("当前data_queue的数据总量为:{}".format(data_queue.qsize()))
        # print("data_queue的数据为:{}".format(data_queue.qsize()))


if __name__ == '__main__':
    main()

对于异步I/O操作,这部分涉及较深,需了解关于事件循环、调度以及操作系统等方面的知识,仍在坚持学习,希望日后有所收获~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值