异步爬虫
发起一个请求,不用等待它按序操作结束,当它结束时会通知。实现方式是通过单线程+协程实现异步IO操作。
相关概念
同步与异步
同步:不同程序为了完成任务,在执行过程中需要靠某种通信方式以协调一致,被称为同步执行。
异步:不同程序为了完成任务,各任务之间过程中无需通信协调也能完成,被称为异步执行。
可理解为同步是串行,按照ABCD顺序执行;异步是并行,ABCD同时进行
阻塞与非阻塞
阻塞:程序在等待某个操作完成期间,无法分身执行别的任务,被称为阻塞
非阻塞:程序在执行操作时,不能立刻得到结果之前,该调用不会阻塞当前线程
基于线程池
个人简单理解就是,线程多了,放在一个池里方便管理,于是就有了线程池。
搭建模拟网站
基于Flask框架
- 在templates文件夹里创建一个HTML,把它作为模拟网站
- Mark directory as template folder
- 实例化一个app,开启调试模式
- 创建视图函数&路由地址
- 运行(只保存不运行服务器未开启)
from flask import Flask,render_template
from time import sleep
# 实例化一个App
app = Flask(__name__)
# 创建视图函数&路由地址
@app.route('/banban')
def index_01():
sleep(2)
return render_template('shiwen.html')
@app.route('/shoushou')
def index_02():
sleep(2)
return render_template('shiwen.html')
@app.route('/huhu')
def index_03():
sleep(2)
return render_template('shiwen.html')
if __name__ == '__main__':
# debug=True 表示开启调试模式:服务器端代码被修改后按下保存进会自动重启服务
app.run(debug=True)
线程池
注:该处存疑,
multiprocessing.dummy.pool
有认为是线程池有认为是进程池,在这里暂且把它当做是线程池
multiprocessing.dummy.Pool.map(func,iterable [,chunksize ] )
此方法将iterable内的每一个对象作为单独的任务提交给线程池,map()支持1个可迭代的参数,如果需要多参数,可以将func作为键值对写入。
在这里,可以使用func(callback)对iterable(alist)列表中的每一个元素进行指定形式的异步操作,map()返回值是一个列表。如果存在1个以上的map(),map()里是异步操作,map()与map()间是串行(按顺序执行)的。
线程池
- 将请求的网站封装
- 定义一个访问类
- 在同步里,按顺序一个接一个执行
- 在异步里,同时执行(乱序执行)
import requests
import time
from multiprocessing.dummy import Pool
# multiprocessing.dummy 多线程
# 非阻塞方法:multiprocessing.dummy.Pool.apply_async() 和 multiprocessing.dummy.Pool.imap()
# 阻塞方法:multiprocessing.dummy.Pool.apply()和 multiprocessing.dummy.Pool.map()
# multiprocessing.dummy.Pool.apply_async()可以有返回值,apply(),map(),imap()不可以设置返回值
urls = [
'http://127.0.0.1:5000/banban',
'http://127.0.0.1:5000/shoushou',
'http://127.0.0.1:5000/huhu'
]
def get_request(url):
page_text = requests.get(url=url).text
return len(page_text)
# 同步代码
# if __name__ == '__main__':
# start = time.time()
#
# for url in urls:
# res = get_request(url)
# print(res)
#
# print('total_time:',time.time()-start)
# 异步代码
if __name__ == '__main__':
start = time.time()
# Pool(3)表示开启线程的数量
pool = Pool(3)
# 使用get_request作为回调函数,需要基于异步的形式对urls列表中的每一个列元素进行操作
# 保证回调函数必须要有一个参数和返回值
result_list = pool.map(get_request, urls)
print(result_list)
print('total_time:',time.time()-start)
同步耗时6秒,即一个接一个的执行程序
异步耗时2秒,多任务封装在线程里,同时执行(乱序执行)
单线程+多任务异步协程
一些概念
特殊的函数
如果一个函数的定义被async修饰后,该函数就变为一个特殊函数。
特点
- async是asyncio模块特有的关键字,不导入无法使用
- 该特殊函数调用后,函数内部的实现语句不会被立即执行
- 该特殊函数被调用后会返回一个协程对象
协程对象
coroutine
特殊函数被调用后会返回一个协程对象,协程对象需要注册到事件循环,由事件循环调用
协程 == 特殊函数 == 一组指定操作
协程 == 一组指定操作
任务对象
task
任务对象是一个高级的协程对象,它是对协程对象的进一步封装,其中包含了任务的各种状态(可挂起)
任务 == 协程 == 特殊函数 == 一组指定操作
任务 == 协程 == 一组指定操作
创建任务对象
asyncio.ensure_future(协程对象)
任务对象的高级之处
- 可以给任务对象绑定回调
task.add_done_callback(task_callback)
- 回调函数的调用时机是任务被执行结束后
- 回调函数的参数只可以有一个,就是该回调函数的调用者(任务对象)
- 使用回调函数的参数调用result()返回的就是任务对象表示的特殊函数return的结果
事件循环对象
event_loop
首先,它是一个对象。
作用
- 可以将多个任务对象注册/装载到事件循环对象中
- 如果开启了循环,则其内部注册/装载的任务对象表示的指定操作就会被基于异步的被执行
创建方式
loop = asyncio.get_event_loop()
注册/启动
loop.run_until_complete(task)
wait()
作用:将任务列表中的任务对象附于可被挂起的权限,只有任务对象被赋予这个权限后才能被挂起。
挂起:将当前的任务对象交出cpu的使用权。
await关键字
在特殊函数内部,凡是阻塞操作前都必须使用await进行修饰,可以保证阻塞操作在异步执行过程中不会被跳过。
注意:在特殊函数内部不可以出现不支持异步模块对应的代码,否则会中断整个异步效果。
aiohttp
它是一个支持异步的网络请求模块。
可以在anaconda-environments-base(root)里查找是否存在该模块
如果没有,则全局搜索后直接下载即可
一些测试
注册/开启事件循环
import asyncio
import requests
import time
async def get_request(url):
print('正在请求的url:',url)
time.sleep(2)
print('请求结束:',url)
return 'banban'
# 回调函数的封装
# 参数t:任务对象 -- 也就是该回调函数的调用者
def task_callback(t):
print('i`m task_callback(),参数是:',t)
# return的是协程对象(任务对象)的返回值
print('t.result()返回的是:',t.result())
if __name__ == '__main__':
# 随机访问1个网站,c就是一个协程对象
c = get_request('www.1.com')
# 任务对象就是对协程对象的进一步封装
task = asyncio.ensure_future(c)
# 给任务对象task 绑定一个回调对象
task.add_done_callback(task_callback)
# 创建事件循环对象
loop = asyncio.get_event_loop()
# 将任务对象(协程对象的封装)注册到事件循环对象中并开启事件循环
loop.run_until_complete(task)
多任务(没有异步效果)
import time
import requests
import asyncio
'''
在特殊函数内部不可以出现不支持异步模块对应的代码,否则会中断整个异步效果
在特殊函数内部,凡是阻塞操作前都必须使用await进行修饰。await就可以保证阻塞操作在异步执行的过程中不会被跳过
'''
async def get_request(url):
print('正在请求的url:',url)
# time.sleep(2) #不支持异步模块的代码
await asyncio.sleep(2) # 支持异步模块的代码
print('请求结束:',url)
return 'banban'
urls = [
'www.1.com',
'www.2.com',
'www.3.com'
]
if __name__ == '__main__':
start = time.time()
tasks = []
for url in urls:
c = get_request(url) #创建协程对象
task = asyncio.ensure_future(c) #封装为任务对象
tasks.append(task) #加入多任务列表
# 创建事件循环对象
loop = asyncio.get_event_loop()
# 使用asyncio.wait()封装多任务列表后,注册并开启事件循环,wait可以将任务列表中的任务对象进行挂起操作
loop.run_until_complete(asyncio.wait(tasks))
print('total_time:',time.time()-start)
多任务的异步爬虫
使用多任务的异步协程爬取数据实现套路:
- 可以先使用requests模块将待请求数据对应的url封装到有个列表中(同步)
- 可以使用aiohttp模式将列表中的url进行异步的请求和数据解析(异步)
import requests
import time
import asyncio
import aiohttp
from lxml import etree
urls =[
'http://127.0.0.1:5000/banban',
'http://127.0.0.1:5000/shoushou',
'http://127.0.0.1:5000/huhu'
]
# async def get_request(url):
# #requests是一个不支持异步的模块
# page_text = requests.get(url).text
# return page_text
async def get_request(url):
#实例化1个请求对象 注意with as写法,且要async修饰
async with aiohttp.ClientSession() as sess:
#调用get请求返回1个响应对象
# requests与aiohttp的get/post(url, headers,params/data一致,aiohttp是proxy="字符串 http://ip:port",requests是proxies)
async with await sess.get(url=url) as response:
# text()获取了字符串形式的响应数据
# read()获取byte类型的响应数据
page_text = await response.text()
return page_text
# 解析函数的封装,使用任务对象的回调函数实现数据解析
# 多任务的架构中数据的爬取是封装在特殊函数中,我们一定要保证数据请求结束后,在实现数据解析。
def parse(t):
# 特殊函数返回的是页面源码数据
page_text = t.result()
tree = etree.HTML(page_text)
parse_text = tree.xpath('//a[@id="feng"]/text()')[0]
print(parse_text)
if __name__ == '__main__':
start = time.time()
tasks = []
for url in urls:
#创建协程对象
c = get_request(url)
#将协程对象封装为任务对象
task = asyncio.ensure_future(c)
#给任务对象绑定一个回调对象
task.add_done_callback(parse)
#将任务对象添加到多任务列表
tasks.append(task)
# 创建事件循环对象
loop = asyncio.get_event_loop()
# 使用asyncio.wait()封装多任务列表后,注册并开启事件循环
loop.run_until_complete(asyncio.wait(tasks))
print('total_time:',time.time()-start)
在写代码时还是常犯几个错误,url里各地址间没有加, 导致没有加载,loop没有跳出循环,没有做出异步效果,还需要不断改正!