0x00:异步爬虫概述
目的:在爬虫中使用异步实现高性能的数据爬取操作。
先来看一个单线程、串行
方式的爬虫:
import requests
headers = {
'User-Agent':'xxx'
}
urls = {
'xxxx'
'xxxx'
'xxxx'
}
def get_content(url):
print("正在爬取:",url)
#get方法是一个阻塞的方法
reponse = requests.get(url=url,headers=headers)
if reponse.status_code == 200:
return reponse.content
def parse_content(content):
print("响应数据的长度为:",len(content))
for url in urls:
content = get_content(url)
parse_content(content)
这段爬虫就是一个一个去请求,如果前面在请求了后面的url
只能等待,这样爬取数据的效率会很低,所以使用异步操作爬取数据来提高效率。
0x01:多进程、多线程
优点:可以为相关阻塞的操作单独开启线程或进程,这样阻塞操作就可以异步执行。
缺点:无法无限制的开启多线程或多进程。
0x02:线程池、进程池
优点:可以降低系统对进程或者线程创建和销毁的一个频率,从而降低系统的开销。
缺点:池中线程或进程的数量是有上限。
0x03:线程池的基本使用
先通过两段爬虫代码进行对比一下
单线程串行方式执行
import time
def get_page(str):
print("正在下载:",str)
time.sleep(2)
print("下载成功:",str)
name_list = ['lemon','shy','good','nice']
start_time = time.time()
for i in range(len(name_list)):
get_page(name_list[i])
end_time = time.time()
print('%d secode'% (end_time-start_time))
使用线程池的方式执行
import time
#导入线程池模块对应的类
from multiprocessing.dummy import Pool
start_time = time.time()
def get_page(str):
print("正在下载:",str)
time.sleep(2)
print("下载成功:",str)
name_list = ['lemon','shy','good','nice']
#实例化一个线程池对象
pool = Pool(4)#四个线程
#将列表中每一个列表元素传递给get_page进行处理
pool.map(get_page,name_list)
end_time = time.time()
print(end_time-start_time)
对比便可以看出线程池提高的效率
0x04:线程池应用
下面就通过爬取一些短视频来进行练习
爬取生活区的这几个视频,先进行观察,点开视频都有详细的url
链接,所以先要爬取出各个视频对应的详情页,再进行处理ul
标签下的各个li
标签包含有a
标签,a
标签中的属性又含有链接和名字
,所以先将所有li标签提取出来,再对各个链接和名称进行处理,如果不想分析的话,也可以这样
再稍微处理一下就可以了
import requests
from lxml import etree
if __name__ == '__main__':
url = 'https://www.pearvideo.com/category_5'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}
page_text = requests.get(url=url,headers=headers).text
tree = etree.HTML(page_text)
li_list = tree.xpath('//*[@id="listvideoListUl"]/li')
for li in li_list:
detail_url = 'https://www.pearvideo.com/'+li.xpath('./div/a/@href')[0]
name = li.xpath('./div/a/div[2]/text()')[0]+'.mp4'
print(detail_url,name)
通过这段代码,便可以得到视频详情页和名称,再来观察视频详情页
找到了链接所在的位置,但是当脚本爬取时发现返回空值,那查看一下视频是不是动态加载出来的
刚才看到视频的链接地址是在video
标签中的并且链接是以MP4结尾的,那就在响应数据中查找一下MP4
并不在video
标签中,而是一组JS代码,因此这个video
标签一定是动态加载出来的,那接下来就去解析这个JS代码,从JS代码中提取出视频链接,但是Xpath\bs4
是无法处理JS数据的,所以使用正则表达式去解析
#将这串JS代码复制下来
var contId="1677300",liveStatusUrl="liveStatus.jsp",liveSta="",
playSta="1",autoPlay=!1,isLiving=!1,isVrVideo=!1,hdflvUrl="",
sdflvUrl="",hdUrl="",sdUrl="",ldUrl="",
srcUrl="https://video.pearvideo.com/mp4/third/20200529/cont-1677300-15195380-142357-hd.mp4",
vdoUrl=srcUrl,skinRes="//www.pearvideo.com/domain/skin",videoCDN="//video.pearvideo.com";
#解析
ex = 'srcUrl="(.*?)",vdoUrl'
正则解析完成后,就可以得出这个视频链接和名称,那根据上面所了解的线程池的作用,处理阻塞且耗时的操作,请求视频资源的话有的很大,所以会用到线程池如果进行持久化保存就更需要线程池,那就加上线程池,最终代码如下:
import requests
from lxml import etree
import re
from multiprocessing.dummy import Pool
#需求:爬取梨视频的视频数据
#原则:线程池处理的是阻塞且耗时的操作
if __name__ == '__main__':
url = 'https://www.pearvideo.com/category_5'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}
page_text = requests.get(url=url,headers=headers).text
tree = etree.HTML(page_text)
li_list = tree.xpath('//*[@id="listvideoListUl"]/li')
urls = []#存储所有视频的链接和名称
for li in li_list:
detail_url = 'https://www.pearvideo.com/'+li.xpath('./div/a/@href')[0]
name = li.xpath('./div/a/div[2]/text()')[0]+'.mp4'
# print(detail_url,name)
#对详情页进行请求
detail_page_text = requests.get(url=detail_url,headers=headers).text
#解析出视频的地址
ex = 'srcUrl="(.*?)",vdoUrl'
video_url = re.findall(ex,detail_page_text)[0]
# print(video_url)
#封装成字典,再添加到列表中
dic = {
'name':name,
'url':video_url
}
urls.append(dic)
#添加一个方法
def get_video_data(dic):
url = dic['url']
print(dic['name'],'正在下载。。。')
data = requests.get(url=url,headers=headers).content
#持久化存储
with open(dic['name'],'wb') as fp:
fp.write(data)
print(dic['name'],'下载成功!')
#使用线程池对视频数据进行请求(较为耗时的阻塞操作)
pool = Pool(4)
pool.map(get_video_data,urls)
#关闭线程池
pool.close()
#主线程等待子线程
pool.join()
0x05:单线程+异步协程
event_loop
: 事件循环,相当于一个无限循环,可以把一些函数注册到这个事件循环上,当满足某些条件的时候,函数就会被循环执行。
coroutine
:协程对象,可以将协程对象注册到事件循环中,它会被事件循环调用。可以使用async
关键字来定义一个方法,这个方法在调用时不会立即被执行,而是返回一个协程对象。
-
task
: 任务,它是协程对象的进一步封装,包含了任务的各个状态。 -
future
: 代表将来执行或还没有执行的任务,实际上和task没有本质区别。 -
async
:定义一个协程。 -
await
: 用来挂起阻塞方法的执行。
简单的一个例子:
import asyncio
async def result(url):
print("正在请求的url是",url)
print("请求成功",url)
#async修饰的函数,调用之后返回一个协程对象
c = result('www.baidu.com')
#创建一个事件循环对象
loop = asyncio.get_event_loop()
#需要将协程对象注册到循环事件中,启动事件循环
#run_until_complete函数即可以启动又可以注册
loop.run_until_complete(c)
再看一下task
的使用
import asyncio
async def result(url):
print("正在请求的url是",url)
print("请求成功",url)
#async修饰的函数,调用之后返回一个协程对象
c = result('www.baidu.com')
#task的使用
loop = asyncio.get_event_loop()
#基于loop创建一个task对象
task = loop.create_task(c)
print(task)
#注册到事件循环当中
loop.run_until_complete(task)
print(task)
future
的使用
import asyncio
async def result(url):
print("正在请求的url是",url)
print("请求成功",url)
#async修饰的函数,调用之后返回一个协程对象
c = result('www.baidu.com')
#future
loop = asyncio.get_event_loop()
future = asyncio.ensure_future(c)
print(future)
loop.run_until_complete(future)
print(future)
回调函数
的使用
创建回调函数
add_done_callback
import asyncio
async def result(url):
print("正在请求的url是",url)
print("请求成功",url)
return url
#async修饰的函数,调用之后返回一个协程对象
c = result('www.baidu.com')
#创建一个回调函数
def callback_func(task):
#result返回的就是任务对象中封装的协程对象对应函数的返回值
print(task.result())
#绑定回调
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(c)
#将回调函数绑定到任务对象中
#默认将task对象传递给callback_func函数
task.add_done_callback(callback_func)
loop.run_until_complete(task)
0x06:多任务异步协程实现
import asyncio
import time
async def request(url):
print("正在下载",url)
#在异步协程中如果出现了同步模块相关的代码,就无法实现异步操作
# time.sleep(2)
#当asyncio遇到阻塞操作时,必须进行手动挂起
await asyncio.sleep(2)
print("下载完毕",url)
start = time.time()
urls = [
'www.baidu.com',
'www.sogou.com',
'www.bing.com'
]
#任务列表:存放多个任务对象
stasks = []
for url in urls:
#协程对象
c = request(url)
#封装到任务对象中
task = asyncio.ensure_future(c)
stasks.append(task)
loop = asyncio.get_event_loop()
#固定写法,不能直接将task列表直接放入,封装到wait中
loop.run_until_complete(asyncio.wait(stasks))
print(time.time()-start)
最终的下载时间
0x07:aiohttp模块
先创建一个web
服务,用于爬取测试
from flask import Flask
import time
app = Flask(__name__)
@app.route('/a')
def index_a():
time.sleep(2)
return 'Hello lemon'
@app.route('/b')
def index_b():
time.sleep(2)
return 'Hello shy'
@app.route('/c')
def index_c():
time.sleep(2)
return 'Hello theshy'
if __name__ == '__main__':
app.run(threaded=True)
编写爬取脚本:
import asyncio
import requests
import time
start = time.time()
urls = [
'http://127.0.0.1:5000/a','http://127.0.0.1:5000/b','http://127.0.0.1:5000/c'
]
async def get_page(url):
print("正在爬取", url)
reponse = requests.get(url)
print("爬取成功",reponse.text)
tasks = []
for url in urls:
c = get_page(url)
task = asyncio.ensure_future(c)
tasks.append(task)
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print('总耗时',end-start)
刚开始这样写觉得也没什么问题,但一运行发现不是异步操作
问题就出现在requests.get
这个地方 ,requests.get
是基于同步,必须使用基于异步的网络请求模块进行请求url
的请求发送,所以就需要使用aiohttp
模块
import asyncio
import requests
import time
import aiohttp
start = time.time()
urls = [
'http://127.0.0.1:5000/a','http://127.0.0.1:5000/b','http://127.0.0.1:5000/c'
]
async def get_page(url):
async with aiohttp.ClientSession() as session:
#headers,params/data,proxy='http://ip:port'
#请求有阻塞,需要手动挂起
async with await session.get(url) as reponse:
#text()返回字符串形式的响应数据
#read()返回二进制形式的响应数据
#json()返回的就是json对象
#注意:在获取响应数据操作之前一定要使用await进行手动挂起
page_text = await reponse.text()
print(page_text)
tasks = []
for url in urls:
c = get_page(url)
task = asyncio.ensure_future(c)
tasks.append(task)
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print('总耗时',end-start)