Bootstrap

Python异步爬虫之协程抓取妹子图片(aiohttp、aiofiles)

 

 

目录

前言

一、什么是协程?

二、协程的优势

三、代码分析

1.引入库

2.获取所有时间线的链接

3.获取一个时间线中所有相册的链接

4.获取一个相册中所有的图片链接以及相册的名字

5.下载并保存图片

6.main函数

7.主方法

四、完整代码


前言

在爬虫的过程中,效率是一个很关键的问题,最常用的是多线程、多进程、线程池、进程池等等。这篇文章主要介绍使用协程来完成抓取妹子图片。


 

一、什么是协程?

协程,英文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()

 

;