前一篇记录了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操作,这部分涉及较深,需了解关于事件循环、调度以及操作系统等方面的知识,仍在坚持学习,希望日后有所收获~