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))

Python爬虫之旅_高性能异步爬虫_数据

使用线程池的方式执行

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)

Python爬虫之旅_高性能异步爬虫_ide_02
对比便可以看出线程池提高的效率

0x04:线程池应用

下面就通过爬取一些短视频来进行练习
Python爬虫之旅_高性能异步爬虫_事件循环_03
爬取生活区的这几个视频,先进行观察,点开视频都有详细的url链接,所以先要爬取出各个视频对应的详情页,再进行处理
Python爬虫之旅_高性能异步爬虫_python_04
ul标签下的各个li标签包含有a标签,a标签中的属性又含有链接和名字,所以先将所有li标签提取出来,再对各个链接和名称进行处理,如果不想分析的话,也可以这样
Python爬虫之旅_高性能异步爬虫_python_05
再稍微处理一下就可以了

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)

通过这段代码,便可以得到视频详情页和名称,再来观察视频详情页
Python爬虫之旅_高性能异步爬虫_ide_06
找到了链接所在的位置,但是当脚本爬取时发现返回空值,那查看一下视频是不是动态加载出来的
Python爬虫之旅_高性能异步爬虫_事件循环_07
刚才看到视频的链接地址是在video标签中的并且链接是以MP4结尾的,那就在响应数据中查找一下MP4
Python爬虫之旅_高性能异步爬虫_事件循环_08
Python爬虫之旅_高性能异步爬虫_ide_09
并不在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()

Python爬虫之旅_高性能异步爬虫_线程池_10

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)

Python爬虫之旅_高性能异步爬虫_事件循环_11
再看一下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)

Python爬虫之旅_高性能异步爬虫_线程池_12
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)

Python爬虫之旅_高性能异步爬虫_事件循环_13
回调函数的使用

创建回调函数
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)

最终的下载时间
Python爬虫之旅_高性能异步爬虫_数据_14

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)

Python爬虫之旅_高性能异步爬虫_数据_15
编写爬取脚本:

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)

刚开始这样写觉得也没什么问题,但一运行发现不是异步操作
Python爬虫之旅_高性能异步爬虫_python_16
问题就出现在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)

Python爬虫之旅_高性能异步爬虫_事件循环_17