目录
前言
在爬虫的过程中,效率是一个很关键的问题,最常用的是多线程、多进程、线程池、进程池等等。这篇文章主要介绍使用协程来完成抓取妹子图片。
一、什么是协程?
协程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程 。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是用户执行)。
二、协程的优势
优势就是性能得到了很大的提升,不会像线程切换那样消耗资源。协程的开销远远小于线程的开销。协程本质是单线程,在不占用更多系统资源的情况下,将IO操作进行了挂起,然后继续执行其他任务,等待IO操作完成之后,再返回原来的任务继续执行。
三、代码分析
1.引入库
import requests #同步的网络请求模块
import re #正则模块,用于提取数据
import asyncio #创建并管理事件循环的模块
import aiofiles #可异步的文件操作模块
import aiohttp #可异步的网络请求模块
import os #可调用操作系统的模块
2.获取所有时间线的链接
#定义一个同步函数,使用requests库进行请求
def get_date_list():
#目标链接
url='https://zhaocibaidicaiyunjian.ml/'
#伪装请求头
header={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'}
#发送请求
r=requests.get(url=url,headers=header)
#获取网页页面源代码
htm=r.text
#使用正则模块re解析获得时间线的链接
results=re.findall('''<aside id="archives-2" class="widget widget_archive">(.*?)</aside>''',htm,re.S)
date_list_str=str(re.findall('''<a href=(.*?)>.*?</a>''',str(results)))
date_list=re.findall('''\\'(.*?)\\\\\\\\\'''',date_list_str)
#返回一个所有时间线链接的列表
return date_list
3.获取一个时间线中所有相册的链接
#判断一个时间线页面是否含有第二页(在下面的协程函数get_album_urls中调用)
def hasnext(Responsetext):
if re.findall('''<a class="next page-numbers" href="(.*?)">下一页</a>''', Responsetext):
nextpage = re.findall('''<a class="next page-numbers" href="(.*?)">下一页</a>''',Responsetext)[0]
return nextpage
else:
return None
#async 定义一个协程函数,参数为一个时间线的链接
async def get_album_urls(date_url):
header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'}
#创建一个上下文管理器,并创建一个可异步的session对象
async with aiohttp.ClientSession() as session:
#session的get()的方法与requests库中的用法一样(注:requests库中get()方法的代理参数为字典形式proxies=dict,支持http和https。而session中get()方法的代理参数为字符串,proxy=str。且仅支持http,不支持https)
async with session.get(url=date_url,headers=header) as Response:
#获取网页页面源代码,使用await将网络IO请求挂起,程序继续执行其他任务,等待内容返回后再跳转到此处继续执行
htm=await Response.text()
#使用正则提取所有相册的链接
album_urls=re.findall('''<a href="(.*?)" class="more-link">继续阅读<span class="screen-reader-text">.*?</span></a>''',htm)
#判断此时间线页面是否含有第二页
nextpage=hasnext(htm)
#如果有第二页则提取第二页所有的相册链接
if nextpage:
async with session.get(url=nextpage,headers=header) as Response1:
htm1=await Response1.text()
#列表生成器,将第二页中的所有相册链接加入到列表album_urls中
[album_urls.append(album_url) for album_url in re.findall('''<a href="(.*?)" class="more-link">继续阅读<span class="screen-reader-text">.*?</span></a>''',htm1)]
#返回一个时间线中所有的相册链接
return album_urls
4.获取一个相册中所有的图片链接以及相册的名字
#async 定义一个协程函数,参数为一个相册链接
async def get_pic_urls_and_title(album_url):
header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'}
#创建一个上下文管理器,并创建一个可异步的session对象
async with aiohttp.ClientSession() as session:
#session的get()的方法与requests库中的用法一样(注:requests库中get()方法的代理参数为字典形式proxies=dict,支持http和https。而session中get()方法的代理参数为字符串,proxy=str。且仅支持http,不支持https)
async with session.get(url=album_url,headers=header) as Response:
#获取网页页面源代码,使用await将网络IO请求挂起,程序继续执行其他任务,等待内容返回后再跳转到此处继续执行
htm=await Response.text()
#使用正则提取出所有的图片地址以及相册的名字
pic_urls=re.findall('''<img src="(.*?)" alt=".*?" border="0" />.*?''',htm,re.S)
title=re.findall('''<h1 class="entry-title">(.*?)</h1>''',htm,re.S)[0]
#返回所有的图片地址以及相册的名字
return pic_urls,title
5.下载并保存图片
#定义一个协程函数,参数为一个相册的所有图片地址以及相册的名字
async def download(pic_urls,title):
header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'}
#为一个相册创建一个文件夹
dir_name = title
#判断是否有这个文件夹,有则结束,否则创建一个文件夹并将图片放入
if os.path.exists(dir_name):
print(dir_name + '这个图片夹已存在')
return False
elif not os.path.exists(dir_name):
os.mkdir(dir_name)
print(f'--------正在下载: {dir_name}--------')
#对每张照片进行异步请求
for pic_url in pic_urls:
#图片的名字取图片的链接地址
pic_name = pic_url.split('/')[-1]
async with aiohttp.ClientSession() as session:
async with session.get(url=pic_url, headers=header) as Response:
#创建一个上下文管理器,并创建一个可异步的文件对象
async with aiofiles.open(file=dir_name + '/' + pic_name, mode='wb') as f:
#因为Response对象的read()和text()方法会将响应一次性全部读入内存,这会导致内存爆满,导致卡顿,影响效率。
#因此采取字节流的形式,每次读取4096个字节并写入文件
while True:
#遇到IO阻塞的情况则挂起,等待内容返回再跳转到此处继续执行
pic_stream = await Response.content.read(4096)
#如果读取完毕之后,则跳出此次循环
if not pic_stream:
break
#文件写入为IO操作,挂起后执行其他任务,写入完成后跳转到此处继续执行
await f.write(pic_stream)
6.main函数
#定义一个协程函数,参数为一个时间线链接
async def main(date_url):
#获取一个时间线中所有的相册链接,使用await进行挂起操作(因为此处get_album_urls为一个协程对象,并且内部有IO等待)
album_urls=await get_album_urls(date_url)
#获取每个相册中的相册名字以及所有图片地址
for album_url in album_urls:
#获取一个相册中的所有图片地址以及相册名字,使用await进行挂起操作(因为此处get_pic_urls_and_title为一个协程对象,并且内部有IO等待)
pic_urls,title=await get_pic_urls_and_title(album_url)
#下载一个相册,使用await进行挂起操作(因为此处download为一个协程对象,并且内部有IO等待)
await download(pic_urls,title)
7.主方法
if __name__=="__main__":
#创建一个任务列表
tasks=[]
#使用同步获取所有时间线的地址(因为后面的所有的操作都基于获取到的时间线链接,所以使用同步操作将所有链接获取完毕后再进行接下来的操作)
date_list=get_date_list()
#为每个时间线链接都创建为一个任务对象
for date_url in date_list:
#此处不是立即执行main函数,而是创建了一个协程对象
task=main(date_url)
#将任务添加到任务列表中
tasks.append(task)
#创建一个事件循环,用户接收信息(接收某个任务的状态,未执行?,执行中?,执行完毕?)
loop=asyncio.get_event_loop()
#此处是真正执行任务,等待所有任务执行结束(可以认为是一种固定的写法)
loop.run_until_complete(asyncio.wait(tasks))
#所有任务执行完毕后关闭事件循环,释放资源
loop.close()
四、完整代码
import requests #同步的网络请求模块
import re #正则模块,用于提取数据
import asyncio #创建并管理事件循环的模块
import aiofiles #可异步的文件操作模块
import aiohttp #可异步的网络请求模块
import os #可调用操作系统的模块
#定义一个同步函数,使用requests库进行请求
def get_date_list():
#目标链接
url='https://zhaocibaidicaiyunjian.ml/'
#伪装请求头
header={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'}
#发送请求
r=requests.get(url=url,headers=header)
#获取网页页面源代码
htm=r.text
#使用正则模块re解析获得时间线的链接
results=re.findall('''<aside id="archives-2" class="widget widget_archive">(.*?)</aside>''',htm,re.S)
date_list_str=str(re.findall('''<a href=(.*?)>.*?</a>''',str(results)))
date_list=re.findall('''\\'(.*?)\\\\\\\\\'''',date_list_str)
#返回一个所有时间线链接的列表
return date_list
#判断一个时间线页面是否含有第二页(在下面的协程函数get_album_urls中调用)
def hasnext(Responsetext):
if re.findall('''<a class="next page-numbers" href="(.*?)">下一页</a>''', Responsetext):
nextpage = re.findall('''<a class="next page-numbers" href="(.*?)">下一页</a>''',Responsetext)[0]
return nextpage
else:
return None
#async 定义一个协程函数,参数为一个时间线的链接
async def get_album_urls(date_url):
header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'}
#创建一个上下文管理器,并创建一个可异步的session对象
async with aiohttp.ClientSession() as session:
#session的get()的方法与requests库中的用法一样(注:requests库中get()方法的代理参数为字典形式proxies=dict,支持http和https。而session中get()方法的代理参数为字符串,proxy=str。且仅支持http,不支持https)
async with session.get(url=date_url,headers=header) as Response:
#获取网页页面源代码,使用await将网络IO请求挂起,程序继续执行其他任务,等待内容返回后再跳转到此处继续执行
htm=await Response.text()
#使用正则提取所有相册的链接
album_urls=re.findall('''<a href="(.*?)" class="more-link">继续阅读<span class="screen-reader-text">.*?</span></a>''',htm)
#判断此时间线页面是否含有第二页
nextpage=hasnext(htm)
#如果有第二页则提取第二页所有的相册链接
if nextpage:
async with session.get(url=nextpage,headers=header) as Response1:
htm1=await Response1.text()
#列表生成器,将第二页中的所有相册链接加入到列表album_urls中
[album_urls.append(album_url) for album_url in re.findall('''<a href="(.*?)" class="more-link">继续阅读<span class="screen-reader-text">.*?</span></a>''',htm1)]
#返回一个时间线中所有的相册链接
return album_urls
#async 定义一个协程函数,参数为一个相册链接
async def get_pic_urls_and_title(album_url):
header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'}
#创建一个上下文管理器,并创建一个可异步的session对象
async with aiohttp.ClientSession() as session:
#session的get()的方法与requests库中的用法一样(注:requests库中get()方法的代理参数为字典形式proxies=dict,支持http和https。而session中get()方法的代理参数为字符串,proxy=str。且仅支持http,不支持https)
async with session.get(url=album_url,headers=header) as Response:
#获取网页页面源代码,使用await将网络IO请求挂起,程序继续执行其他任务,等待内容返回后再跳转到此处继续执行
htm=await Response.text()
#使用正则提取出所有的图片地址以及相册的名字
pic_urls=re.findall('''<img src="(.*?)" alt=".*?" border="0" />.*?''',htm,re.S)
title=re.findall('''<h1 class="entry-title">(.*?)</h1>''',htm,re.S)[0]
#返回所有的图片地址以及相册的名字
return pic_urls,title
#定义一个协程函数,参数为一个相册的所有图片地址以及相册的名字
async def download(pic_urls,title):
header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'}
#为一个相册创建一个文件夹
dir_name = title
#判断是否有这个文件夹,有则结束,否则创建一个文件夹并将图片放入
if os.path.exists(dir_name):
print(dir_name + '这个图片夹已存在')
return False
elif not os.path.exists(dir_name):
os.mkdir(dir_name)
print(f'--------正在下载: {dir_name}--------')
#对每张照片进行异步请求
for pic_url in pic_urls:
#图片的名字取图片的链接地址
pic_name = pic_url.split('/')[-1]
async with aiohttp.ClientSession() as session:
async with session.get(url=pic_url, headers=header) as Response:
#创建一个上下文管理器,并创建一个可异步的文件对象
async with aiofiles.open(file=dir_name + '/' + pic_name, mode='wb') as f:
#因为Response对象的read()和text()方法会将响应一次性全部读入内存,这会导致内存爆满,导致卡顿,影响效率。
#因此采取字节流的形式,每次读取4096个字节并写入文件
while True:
#遇到IO阻塞的情况则挂起,等待内容返回再跳转到此处继续执行
pic_stream = await Response.content.read(4096)
#如果读取完毕之后,则跳出此次循环
if not pic_stream:
break
#文件写入为IO操作,挂起后执行其他任务,写入完成后跳转到此处继续执行
await f.write(pic_stream)
#定义一个协程函数,参数为一个时间线链接
async def main(date_url):
#获取一个时间线中所有的相册链接,使用await进行挂起操作(因为此处get_album_urls为一个协程对象,并且内部有IO等待)
album_urls=await get_album_urls(date_url)
#获取每个相册中的相册名字以及所有图片地址
for album_url in album_urls:
#获取一个相册中的所有图片地址以及相册名字,使用await进行挂起操作(因为此处get_pic_urls_and_title为一个协程对象,并且内部有IO等待)
pic_urls,title=await get_pic_urls_and_title(album_url)
#下载一个相册,使用await进行挂起操作(因为此处download为一个协程对象,并且内部有IO等待)
await download(pic_urls,title)
if __name__=="__main__":
#创建一个任务列表
tasks=[]
#使用同步获取所有时间线的地址(因为后面的所有的操作都基于获取到的时间线链接,所以使用同步操作将所有链接获取完毕后再进行接下来的操作)
date_list=get_date_list()
#为每个时间线链接都创建为一个任务对象
for date_url in date_list:
#此处不是立即执行main函数,而是创建了一个协程对象
task=main(date_url)
#将任务添加到任务列表中
tasks.append(task)
#创建一个事件循环,用户接收信息(接收某个任务的状态,未执行?,执行中?,执行完毕?)
loop=asyncio.get_event_loop()
#此处是真正执行任务,等待所有任务执行结束(可以认为是一种固定的写法)
loop.run_until_complete(asyncio.wait(tasks))
#所有任务执行完毕后关闭事件循环,释放资源
loop.close()