文章目录
前言
2021.08.01协程听得有点蒙,弄完第六章。后面还差一点,一会再补上,过了12点就2号了。
2021.08.02补充了代码实现中的⑥⑦。
第六章
1.知识点
高性能异步爬虫
- 目的:在爬虫中使用异步实现高性能的数据爬取操作。
- 异步爬虫的方式:
- 1.多线程、多进程(不建议):
- 好处:可以为相关阻塞的操作单独开启线程或进程,阻塞操作就可以异步执行。
- 弊端:无法无限制的开启多线程或者多进程。
- 2.线程池、进程池(适当的使用):
- 好处:我们可以降低系统对进程或者线程创建和销毁的频率,从而很好地降低系统的开销。
- 弊端:池中线程或进程的数量是有上限的。
- 原则:线程池处理的是阻塞且较为耗时的操作
- 3.单线程 + 异步协程(推荐):
- 一些概念和两个关键字:
- ①event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足某些条件时,函数就会被循环执行。
- ②coroutline:协程对象,我们可以将协程对象注册到事件循环中,它会被事件循环调用。我们可以使用async关键字来定义一个方法,这个方法在调用时不会被立即被执行,而是返回一个协程对象。
- ③task:任务,它是对协程对象的进一步封装,包含了任务的各个状态。
- ④future:代表将来执行或还没有执行的任务,实际上和task没有本质区别。
- ⑤async:定义一个协程。
- ⑥await:用来挂起阻塞方法的执行。
- 一些概念和两个关键字:
- 1.多线程、多进程(不建议):
2.代码实现
①单线程爬取数据
使用单线程爬取数据案例展示:
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
def get_content(url):
print('正在爬取:', url)
# get方法是一个阻塞的方法
response = requests.get(url=url, headers=headers)
print(response.status_code)
if response.status_code == 200:
return response.content
def parse_content(content):
print('相应数据的长度为:', len(content))
if __name__ == "__main__":
urls = [
'https://downsc.chinaz.net/Files/DownLoad/jianli/202107/jianli15646.rar',
'https://downsc.chinaz.net/Files/DownLoad/jianli/202107/jianli15647.rar',
'https://downsc.chinaz.net/Files/DownLoad/jianli/202107/jianli15651.rar',
]
for url in urls:
content = get_content(url)
parse_content(content)
②线程池爬取数据
使用线程池爬取数据案例展示:
# import time
#
#
# # 使用单线程串行方式执行
#
# def get_page(str):
# print("正在下载:", str)
# time.sleep(2)
# print("下载成功", str)
#
#
# if __name__ == "__main__":
# name_list = ['xiaozi', 'aa', 'bb', 'cc']
#
# start_time = time.time()
#
# for i in range(len(name_list)):
# get_page(name_list[i])
#
# end_time = time.time()
# print('%d second' % (end_time - start_time))
import time
# 导入线程池对应的类
from multiprocessing.dummy import Pool
# 使用线程池方式执行
def get_page(str):
print("正在下载:", str)
time.sleep(2)
print("下载成功", str)
if __name__ == "__main__":
start_time = time.time()
name_list = ['xiaozi', 'aa', 'bb', 'cc']
# 实例化一个线程池对象
# (优化后耗时2秒,试了一下,把4改成3耗时4秒,推测是有一条为单线)
pool = Pool(4)
# 将列表中每一个列表元素传递到gert_page进行处理
# map的返回值是get_page函数的返回值,为一个列表,此处不需要进行处理
pool.map(get_page, name_list)
end_time = time.time()
print('%d second' % (end_time - start_time))
③爬虫中应用线程池(动态加载的video标签待解决)
爬虫中应用线程池:
import requests
from lxml import html
etree = html.etree
import re
from multiprocessing.dummy import Pool
# 需求:爬取视频的视频数据
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
}
# 原则:线程池处理的是阻塞且较为耗时的操作
def get_video_data(dic):
url = dic['url']
print(dic['name'], 'downloading...')
data = requests.get(url=url, headers=headers).content
# 持久化存储操作
with open(dic['name'], 'wb') as fp:
fp.write(data)
print(dic['name'], '下载成功')
if __name__ == "__main__":
# 对下述url发起请求解析出视频详情页的url和视频名称
url = 'https://www.pearvideo.com/category_5'
page_text = requests.get(url=url, headers=headers).text
tree = etree.HTML(page_text)
li_list = tree.xpath('//ul[@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)
# 对详情页的url发起请求
detail_page_text = requests.get(url=detail_url, headers=headers).text
# 从详情页中解析出视频的地址(url)
# 用作案例的网站在教程中的那个时间段(一几年,大概是18年)
# 没有video标签,视频的地址在js里面,所以不能用bs4和xpath
# 但是网站已经更新(2021.08.01),我现在学的时候src的Url已经在video标签中了
# 因此此处正则不再适用,咱用xpath修改一下
# 教程代码:
# ex = 'srcUrl="(.*?)",vdoUrl'
# video_url = re.findall(ex, detail_page_text)[0]
# 咱的代码:
# detail_tree = etree.HTML(detail_page_text)
# print(detail_page_text)
# video = detail_tree.xpath('//*[@id="JprismPlayer"]/video')
# print(video)
# video_url = video[0]
# 哈哈,放video标签之后不知道是人家反爬了还是咱的爬取有问题
# 应该是弄得Ajax动态加载
# 爬到的:
# <div class="main-video-box" id="drag_target1">
# <div class="img prism-player" style="height:100% !important;" id="JprismPlayer">
# </div>
# </div>
# F12看到的:
# <div class="main-video-box" id="drag_target1">
# <div class="img prism-player play" style="height: 100% !important; width: 100%;" id="JprismPlayer"
# x-webkit-airplay="" playsinline="" webkit-playsinline="" >
# < video webkit-playsinline="" playsinline="" x-webkit-airplay="" autoplay="autoplay"
# src="https://video.pearvideo.com/mp4/third/20210801/cont-1737209-12785353-091735-hd.mp4"
# style="width: 100%; height: 100%;">
# < / video >
# ...
# </div>
# </div>
# 又挖了一个坑,以后再填qwq
video_url = ''
dic = {
'name': name,
'url': video_url
}
urls.append(video_url)
# 使用线程池对视频数据进行请求(较为耗时的阻塞操作)
pool = Pool(4)
pool.map(get_video_data, urls)
pool.close()
pool.join()
④协程
代码如下:
import asyncio
async def request(url):
print('正在请求的url是', url)
print('请求成功', url)
return url
# async修饰的函数,调用之后返回的一个协程对象
c = request('www.baidu.com')
# # 创建一个事件循环对象
# loop = asyncio.get_event_loop()
#
# # 将协程对象注册到loop中,然后启动loop
# loop.run_until_complete(c)
# # task的使用
# loop = asyncio.get_event_loop()
# # 基于loop创建task任务对象
# task = loop.create_task(c)
# print(task)
#
# loop.run_until_complete(task)
#
# print(task)
# # future的使用
# loop = asyncio.get_event_loop()
# task = asyncio.ensure_future(c)
# print(task)
# loop.run_until_complete(task)
# print(task)
def callback_func(task):
# result返回的是任务对象中封装的协程对象对应函数的返回值
print(task.result())
# 绑定回调
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(c)
# 将回调函数绑定到任务对象中
task.add_done_callback(callback_func)
loop.run_until_complete(task)
⑤多任务异步协程01
代码如下:
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.goubanjia.com'
]
# 任务列表:存放多个任务对象
stasks = []
for url in urls:
c = request(url)
task = asyncio.ensure_future(c)
stasks.append(task)
loop = asyncio.get_event_loop()
# 需要将任务列表封装到wait中
loop.run_until_complete(asyncio.wait(stasks))
print(time.time()-start)
⑥多任务异步协程02
搭建一个简单的web服务器:
from flask import Flask
import time
app = Flask(__name__)
@app.route('/john')
def index_john():
time.sleep(2)
return 'Hello john'
@app.route('/smith')
def index_smith():
time.sleep(2)
return 'Hello smith'
@app.route('/tom')
def index_tom():
time.sleep(2)
return 'Hello tom'
if __name__ == "__main__":
app.run(threaded=True)
异步协程实现代码(此处仍为单线):
import requests
import asyncio
import time
start = time.time()
urls = [
'http://127.0.0.1:5000/john',
'http://127.0.0.1:5000/smith',
'http://127.0.0.1:5000/tom',
]
async def get_page(url):
print('正在下载', url)
# requests.get是基于同步的,必须使用基于异步的网络请求模块进行指定url的请求发送
# aiohttp:基于异步网络请求的模块
response = requests.get(url)
print('下载完毕', response.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)
⑦aiohttp实现多任务异步协程
- aiohttp:基于异步网络请求的模块
- 环境安装:
pip install aiohttp
- 使用环境中的ClientSession
- 与requests模块的区别,调用的不是属性,而是方法:
- text()返回字符串形式的响应数据
- read()返回二进制形式的响应数据
- json()返回的是json对象
代码如下:
import requests
import asyncio
import time
import aiohttp
# 环境安装:pip install aiohttp
# 使用环境中的ClientSession
start = time.time()
urls = [
'http://127.0.0.1:5000/john',
'http://127.0.0.1:5000/smith',
'http://127.0.0.1:5000/tom',
]
async def get_page(url):
async with aiohttp.ClientSession() as session:
# 使用不同的请求类型:
# get()、post()
# 添加相关的参数:
# headers,params/data proxy='http://ip:port'
# 例如:
# async with await session.get(url, params, proxy) as response:
# async with await session.post(url, data, proxy) as response:
async with await session.get(url) as response:
# text()返回字符串形式的响应数据
# read()返回二进制形式的响应数据
# json()返回的是json对象
# 注意: 获取响应数据操作之前一定要使用await进行手动挂起
page_text = await response.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)