很多同学对于异步这个概念只是停留在了“听说很NB”的认知层面上,很少有人能够在项目中真正的使用异步实现高性能的相关操作。接下来,咱们就一起来学习一下,爬虫中如何使用异步实现高性能的数据爬取操作。
其实爬虫的本质就是client发请求批量获取server的响应数据,如果我们有多个url待爬取,只用一个线程且采用串行的方式执行,那只能等待爬取一个结束后才能继续下一个,效率会非常低。
所以单线程下串行多个爬虫任务是比较低效的,是因为爬虫任务是明显的IO密集型(阻塞)程序。那么该如何提高爬取性能呢?查看下述的分析测试!
实验环境创建:
由于网络请求会受到个人电脑或者服务器响应时长不同等因素,我们可以自己创建一个专门用于测试的实验环境,搭建一个flask服务器,爬取本机自己服务器中的数据,避免其他因素的干扰,以便非常明显的观测出异步的效果。
#安装flask模块:pip install flask
from flask import Flask,render_template
from time import sleep
#1.实例化app对象
app = Flask(__name__)
#装饰器中的参数就是路由地址
@app.route('/main')
def main():#视图函数
sleep(2)
return 'i am main'
#一旦启动了服务器后,在浏览器中访问路由地址,在服务器端就会执行视图函数
@app.route('/bobo')
def index1():
sleep(2)
return render_template('test.html')
@app.route('/jay')
def index2():
sleep(2)
return render_template('test.html')
@app.route('/tom')
def index3():
sleep(2)
return render_template('test.html')
if __name__ == "__main__":
app.run()
同步调用:即提交一个任务后就在原地等待任务结束,等到拿到任务的结果后再继续下一行代码,效率低下
import requests
import time
start = time.time()#程序执行开始的时间
urls = ['http://127.0.0.1:5000/bobo',
'http://127.0.0.1:5000/tom',
'http://127.0.0.1:5000/jay']
def get_request(url):
page_text = requests.get(url=url).text
print(len(page_text))
for url in urls:
#发起了三次请求
get_request(url)
print('总耗时:',time.time()-start) #程序运行的总耗时
a. 解决同步调用方案之多线程/多进程
好处:使用多线程(或多进程)。多线程(或多进程)的目的是让每个爬取任务都拥有独立的线程(或进程),这样任何一个爬取任务的阻塞都不会影响其他的爬取任务。
弊端:开启多进程或都线程的方式,我们是无法无限制地开启多进程或多线程的:在遇到要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率。
测试代码:
import requests
import time
from threading import Thread #线程模块
start = time.time()#程序执行开始的时间
urls = ['http://127.0.0.1:5000/bobo',
'http://127.0.0.1:5000/tom',
'http://127.0.0.1:5000/jay']
def get_request(url):
page_text = requests.get(url=url).text
print(len(page_text))
ts = []
for url in urls:
t = Thread(target=get_request,args=(url,))
ts.append(t)
t.start()
for t in ts:#让主线程等在所有的子线程结束后再结束
t.join()
print('总耗时:',time.time()-start) #程序运行的总耗时
b. 解决同步调用方案之线程/进程池
好处:很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。可以很好的降低系统开销。
弊端:“线程池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
通常使用multiprocessing.dummy线程池的方案。
import requests
import time
from multiprocessing.dummy import Pool #线程池模块
start = time.time()#程序执行开始的时间
urls = ['http://127.0.0.1:5000/bobo',
'http://127.0.0.1:5000/tom',
'http://127.0.0.1:5000/jay']
def get_request(url):
page_text = requests.get(url=url).text
print(len(page_text))
pool = Pool(3)
pool.map(get_request,urls)
print('总耗时:',time.time()-start) #程序运行的总耗时
总结:
上述无论哪种解决方案其实没有解决一个性能相关的问题:
解决这一问题的关键在于:
在python3.5之后新增了asyncio模块,可以帮我们检测IO(只能是网络IO【HTTP连接就是网络IO操作】),实现应用程序级别的切换(异步IO)。
接下来让我们来了解下协程的实现,从 Python 3.4 开始,Python 中加入了协程的概念,但这个版本的协程还是以生成器对象为基础的,在 Python 3.5 则增加了 asyncio,使得协程的实现更加方便。首先我们需要了解下面几个概念:
特殊函数:
协程:
任务:
事件循环:
import asyncio
import time
#特殊的函数
async def get_request(url):
print('正在请求的网址是:',url)
time.sleep(2)
print('请求网址结束!')
return 123
#创建了一个协程对象
c = get_request('www.1.com')
#创建任务对象
task = asyncio.ensure_future(c)
#创建事件循环对象
loop = asyncio.get_event_loop()
#将任务对象装载在loop对象中且启动事件循环对象
loop.run_until_complete(task)
任务对象对比协程对象的高级之处重点在于:
可以给任务对象绑定一个回调函数!
回调函数有什么作用?
import asyncio
import time
#特殊的函数
async def get_request(url):
print('正在请求的网址是:',url)
time.sleep(2)
print('请求网址结束!')
return 123
#回调函数的封装:必须有一个参数
def t_callback(t):
#参数t就是任务对象
# print('回调函数的参数t是:',t)
# print('我是任务对象的回调函数!')
data = t.result()#result()函数就可以返回特殊函数内部的返回值
print('我是任务对象的回调函数!,获取到特殊函数的返回值为:',data)
#创建协程对象
c = get_request('www.1.com')
#创建任务对象
task = asyncio.ensure_future(c)
#给任务对象绑定回调函数:add_done_callback的参数就是回调函数的名字
task.add_done_callback(t_callback)
#创建事件循环对象
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
多任务的协程
import asyncio
import time
start = time.time()
urls = [
'www.1.com','www.2.com','www.3.com'
]
async def get_request(url):
print('正在请求:',url)
time.sleep(2)
print('请求结束:',url)
#有了三个任务对象和一个事件循环对象
if __name__ == '__main__':
tasks = []
for url in urls:
c = get_request(url)
task = asyncio.ensure_future(c)
tasks.append(task)
#将三个任务对象,添加到一个事件循环对象中
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
print('总耗时:',time.time()-start)
wait()函数:
await关键字:挂起发生阻塞操作的任务对象。在任务对象表示的操作中,凡是阻塞操作的前面都必须加上await关键字进行修饰!
完整的实现了,多任务的异步协程操作
import asyncio
import time
start = time.time()
urls = [
'www.1.com','www.2.com','www.3.com'
]
async def get_request(url):
print('正在请求:',url)
# time.sleep(2) #time模块不支持异步
await asyncio.sleep(2)
print('请求结束:',url)
#有了三个任务对象和一个事件循环对象
if __name__ == '__main__':
tasks = []
for url in urls:
c = get_request(url)
task = asyncio.ensure_future(c)
tasks.append(task)
#将三个任务对象,添加到一个事件循环对象中
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
print('总耗时:',time.time()-start)
真正的将多任务的异步协程作用在爬虫中
需求:爬取自己服务器中的页面数据,并将其进行数据解析操作
aiohttp:是一个基于网络请求的模块,功能和requests相似,但是,requests是不支持异步的,而aiohttp是支持异步的模块。
环境安装:pip install aiohttp
具体用法:
1.先写大致加购
with aiohttp.ClientSession() as sess:
#基于请求对象发起请求
#此处的get是发起get请求,常用参数:url,headers,params,proxy
#post方法发起post请求,常用参数:url,headers,data,proxy
#发现处理代理的参数和requests不一样(注意),此处处理代理使用proxy='http://ip:port'
with sess.get(url=url) as response:
page_text = response.text()
#text():获取字符串形式的响应数据
#read():获取二进制形式的响应数据
return page_text
2.在第一步的基础上补充细节
在每一个with前加上async关键字
在阻塞操作前加上await关键字
完整代码:
async def get_request(url):
#requests是不支持异步的模块
# response = await requests.get(url=url)
# page_text = response.text
#创建请求对象(sess)
async with aiohttp.ClientSession() as sess:
#基于请求对象发起请求
#此处的get是发起get请求,常用参数:url,headers,params,proxy
#post方法发起post请求,常用参数:url,headers,data,proxy
#发现处理代理的参数和requests不一样(注意),此处处理代理使用proxy='http://ip:port'
async with await sess.get(url=url) as response:
page_text = await response.text()
#text():获取字符串形式的响应数据
#read():获取二进制形式的响应数据
return page_text
多任务异步爬虫的完整代码实现:
import requests
import asyncio
import time
from lxml import etree
import aiohttp
start = time.time()
urls = [
'http://127.0.0.1:5000/bobo',
'http://127.0.0.1:5000/jay',
'http://127.0.0.1:5000/tom'
]
#该任务是用来对指定url发起请求,获取响应数据
async def get_request(url):
#requests是不支持异步的模块
# response = await requests.get(url=url)
# page_text = response.text
#创建请求对象(sess)
async with aiohttp.ClientSession() as sess:
#基于请求对象发起请求
#此处的get是发起get请求,常用参数:url,headers,params,proxy
#post方法发起post请求,常用参数:url,headers,data,proxy
#发现处理代理的参数和requests不一样(注意),此处处理代理使用proxy='http://ip:port'
async with await sess.get(url=url) as response:
page_text = await response.text()
#text():获取字符串形式的响应数据
#read():获取二进制形式的响应数据
return page_text
def parse(t):#回调函数专门用于数据解析
#获取任务对象请求到的页面源码数据
page_text = t.result()
tree = etree.HTML(page_text)
a = tree.xpath('//a[@id="feng"]/@href')[0]
print(a)
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()
loop.run_until_complete(asyncio.wait(tasks))
print('总耗时:',time.time()-start)
使用uvloop加速(了解)
import uvloop
loop = asyncio.get_event_loop()
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
loop.run_until_complete(asyncio.wait(tasks))