协程的概念
协程是在一个线程内切换任务,所需要的资源更少,切换速度更快。
爬虫任务非常适合用协程来实现,原因是请求目标网站的时候需要等待返回,这段时间就可以切换到其他任务(例如解析已经请求成功的页面等)。
应用实例
aiohttp是发起异步请求的库,async
修饰的函数表明该函数是异步函数,里面存在异步操作。
目标网站为: 中央结算系统持股记录查询服务
请求页面的代码如下:
async def get_page(day, code):
# 发起post请求需要的参数
data = {
"txtStockCode": code,
"txtShareholdingDate": day.replace('-',''),
"__EVENTTARGET": "btnSearch",
"sortBy":"shareholding",
"sortDirection":"desc",
}
html = None
try:
with(await sem): # 控制并发的信号量
# 用aiohttp发起异步请求
async with aiohttp.ClientSession() as session:
# 如果请求失败,最大重试次数
maxretry = 3
while(maxretry>0):
maxretry = maxretry - 1
async with session.post(url = url, headers=headers, data=data,timeout=20) as response:
if response.status==200:
# await 表示异步等待
html = await response.text()
break
else:
asyncio.sleep(5)
if maxretry<0:
# 请求失败的股票代码,需要做后续处理
failed_codes.append(code)
print("failed code :",code)
except Exception as e:
failed_codes.append(code)
print(code,e)
return (day, code, html) # 异步函数返回的结果
上面的请求和普通的请求有什么区别呢?
首先,函数的定义用 async
修饰表明该函数为异步操作函数,其中有异步等待的内容,请求成功之后,需要返回页面,这里用await
修饰,表明异步等待(意思是可以先做别的事情,等页面返回成功之后再从此处继续执行)
请求成功之后,还需要解析页面,解析函数如下:
# 请求结束之后的回调函数
def parse_page(task):
global datas # 存储解析数据的全局变量
# 获得请求成功侯的页面
day,code,html = task.result()
if html is None:
return
soup = BeautifulSoup(html,'lxml')
# 解析过程略
if len(datas)>5000:
# 写入数据
write_to_csv(day,datas)
datas = []
然后将两者结合起来:
def run(day,codes):
"""爬取某天,指定股票代码的数据,
Args:
day (str): 2022-10-10
codes (list): stock_code, 为香港5位数代码
"""
global failed_codes
global datas
failed_codes = [] # 清空上一次失败代码
datas = [] # 清空上一次的数据残留
tasks = []
for code in codes:
# 构造协程任务列表
task = asyncio.ensure_future(get_page(day,code))
# 任务结束之后的回调函数:请求成功之后,立即解析页面
task.add_done_callback(parse_page)
tasks.append(task)
# 获取事件循环
loop = asyncio.get_event_loop()
# 启动任务
loop.run_until_complete(asyncio.wait(tasks))
write_to_csv(day,datas) # 最后剩余数据也要写入
框架总结
如果读者对上面的应用实例不理解,那么就无法应用到自己的爬虫任务种,这里总结一般爬虫任务种使用协程的代码框架:
import aiohttp
import asyncio
# 1. 异步发起请求,以post请求为例
async def get_page(param):
# 发起post请求需要的参数
data = {
param(自行构造请求需要的参数)
}
html = None
try:
with(await sem): # 控制并发的信号量
# 用aiohttp发起异步请求
async with aiohttp.ClientSession() as session:
async with session.post(url = url, headers=headers, data=data,timeout=20) as response:
if response.status==200:
# await 表示异步等待
html = await response.text()
except Exception as e:
print(e)
return html # 异步函数返回的结果
# 2.解析页面数据,这里传入的参数是协程任务
def parse_page(task):
# 获得请求成功侯的页面
html = task.result()
# 利用bs4等进行解析操作
result = parse(html)
# 将解析的数据持久化保存
writ_to_local(result)
# 3.构造协程任务列表
def run(params):
tasks = []
for param in params:
# 构造协程任务列表
task = asyncio.ensure_future(get_page(param))
# 任务结束之后的回调函数:请求成功之后,立即解析页面
task.add_done_callback(parse_page)
tasks.append(task)
# 获取事件循环
loop = asyncio.get_event_loop()
# 启动任务
loop.run_until_complete(asyncio.wait(tasks))
注意:代码中的参数param,params,parse函数,writ_to_local函数等都是示意,具体情况需要具体分析,自己写对应的函数。
总结
可以看到,最为关键的是最后的run
函数,读者只需要根据自己的任务,构造合适的请求函数和解析函数,应用到run
函数中,即可启动协程任务。
并发信号量 sem是一个全局变量,用来控制协程的数量,或者说爬虫的速度。
还可以参考笔者另一篇文章 用asyncio和aiohttp异步协程爬取披露易网站港资持股数据