Web爬虫 基于线程池的异步爬虫介绍 基于多任务协程爬虫介绍

首页 » Python » Web爬虫 基于线程池的异步爬虫介绍 基于多任务协程爬虫介绍

基于线程池的异步爬虫

异步爬虫有两种,一种是基于线程池的异步爬虫,另一种是基于多任务异步协程。这篇文章先来介绍第一种异步爬虫,首先我们需要引入线程池:

from multiprocessing.dummy import Pool

dummy是虚拟的进程,指的是进程池

为了更好的看到异步的效果,需要自己搭建一个服务器,鳄鱼君Ba这里就用Flas来演示,它是一个非常灵活而又轻量的框架。安装方法也非常简单,使用pip install flask命令就可以进行安装。我们在当前目录下创建一个templates文件存放index.html,随便找一个HTML页面也可以,需要有内容。然后创建以下代码,让程序阻塞3秒为例能更好的看到效果:

from flask import Flask,render_template
from time import sleep
#实例化一个flask的实例对象
app=Flask(__name__)
@app.route('/index1')
def index_1():
    sleep(3)
    return render_template('index.html')
@app.route('/index2')
def index_2():
    sleep(3)
    return render_template('index.html')
@app.route('/index3')
def index_3():
    sleep(3)
    return render_template('index.html')
if __name__ == '__main__':
    #debug=True表示为调试模式,保存就会重启服务
    app.run(debug=True)

这时候访问相关页面就会等3秒再加载,然后我们先来编写一个简单的爬虫代码,记录一下时间,这里的时间应该超过9秒吧,通过下面代码就可以测试出来:

import time,requests
urls=['http://127.0.0.1:5000/index1',
      'http://127.0.0.1:5000/index2',
       'http://127.0.0.1:5000/index3',
    ]
def get_urls(url):
    page_html=requests.get(url).text #直接请求数据
    print(len(page_html)) #打印数据的数量
if __name__ == '__main__':
    start_time=time.time()
    for url in urls:
        get_urls(url)
    print('总耗时:%s' % (time.time()-start_time))

以上代码可以发现,我们以前编写的代码都是同步进行的,非常耗时接下来修改一下,看一下异步的效果是什么:

import time,requests
from multiprocessing.dummy import Pool
urls=['http://127.0.0.1:5000/index1',
      'http://127.0.0.1:5000/index2',
       'http://127.0.0.1:5000/index3',
    ]
def get_urls(url):
    page_html=requests.get(url).text #直接请求数据
    return len(page_html) #打印数据的数量
if __name__ == '__main__':
    start_time = time.time()
    pool=Pool(3) #实例化一个线程池 参数代表数量
    #pool.map(func,alist)  
    # 参数1:回调函数,参数2:可迭代对象
    # 作用:可以将alist中的每一个元素依次传递给func函数作为参数
    # 然后回调函数会异步的对列表中的元素进行相关操作运算
    # map的返回值就是回调函数返回的所有结果
    page_content=pool.map(get_urls,urls)
    print(page_content)
    print('总耗时:%s' % (time.time()-start_time))

运行代码,就会发现请求是异步的,时间为3s,自行尝试效果!

基于多任务异步协程

接下来我们再来看一下基于多任务异步协程。首先需要装asyncio,使用命令pip install asyncio即可。然后需要知道下面的基本语法,在学习整个多任务异步爬虫时,你必须要搞清楚它们的含义,否则你可能很难理解代码:

特殊函数:如果一个函数的定义被async关键字修饰,则该函数就变成了一个特殊的函数。特殊之处:该函数调用后函数内部的实现语句不会被立即执行;该函数被调用后会返回一个协程对象。
 
协程:对象,该对象是特殊函数调用后返回的协程对象。协程对象==特殊函数。一个函数表示指定的一组操作,那么一个协程对象表示一组特定的操作。
如何创建一个协程对象?
调用特殊函数的返回值
 
任务对象:高级的协程对象,即任务对象==协程对象==特殊的函数任务对象==特殊的函数。任务对象也表示一组特定的操作,高级在哪后面再说!
如何创建一个任务对象?
task=asyncio.ensure_future(c) # c值协程对象
 
事件循环:对象。如何创建该对象?loop=asyncio.get_event_loop()。作用:用来装载任务(协程)对象,可以将事件循环当做是一个容器,容器中存放的是多个任务对象;如果事件循环存放了多个任务对象且事件循环启动后,则事件循环对象就可以异步的将每一个任务对象对应的指定操作进行执行。
如何将任务对象存储且启动事件循环对象?
loop.run_until_complete(task) #将一个任务对象进行了存储。
Web爬虫 基于线程池的异步爬虫介绍 基于多任务协程爬虫介绍

特殊函数

这里还是基于上面的代码做修改,正常的函数这里就不再说了,主要讲一下特殊的函数,特殊函数需要用async修饰,并且不会被立即执行,并且该函数会返回一个coroutine协程对象:

import requests,asyncio,time

async def get_urls(url):
    print('get_urls被调用')
    page_content=requests.get(url).text #直接请求数据
    return page_content
if __name__ == '__main__':
    c=get_urls('http://127.0.0.1:5000/index1')
    print(c)
# <coroutine object get_urls at 0x000000000322FE48>
# sys:1: RuntimeWarning: coroutine 'get_urls' was never awaited

结合以上解释,修改代码如下:

import requests,asyncio
async def get_urls(url):
    print('get_urls被调用')
    page_content=requests.get(url).text #直接请求数据
    return page_content
if __name__ == '__main__':
    # 创建一个协程对象
    c=get_urls('http://127.0.0.1:5000/index1')
    # print(c)
    # 创建一个任务对象(基于已有的协程对象创建任务对象
    task=asyncio.ensure_future(c) # c值协程对象

    # 创建一个事件循环对象
    loop=asyncio.get_event_loop()

    # 将task存储到event_loop中且启动该对象
    loop.run_until_complete(task)

运行以上代码,你会发现,事件循环启动后,任务对象对应的指定操作(特殊函数)已经被执行,但是如果这组指定操作中有返回值,返回值如何获取呢?那么任务对象在事件循环中是否基于异步执行的呢?

获取特殊函数中的返回值

需要基于任务对象的回调函数,那么如何给任务对象绑定回调函数?
task.add_done_callback(parse)
parse:就是回调函数,该函数必须要有一个参数,参数就是该函数的绑定者,即任务对象。
result()返回的就是特殊函数的返回值。
Web爬虫 基于线程池的异步爬虫介绍 基于多任务协程爬虫介绍
import requests,asyncio

async def get_urls(url):
    print('get_urls被调用')
    page_content=requests.get(url).text #直接请求数据
    return page_content

# 定义一个任务对象的回调函数
# 回调函数必须要要有一个参数 名字随意
# 该参数表示的就是该函数的绑定者 即任务对象task
def parse(task):
    # result():返回的就是特殊函数的返回值
    page_content=task.result()
    print('I am Task callback! 特殊函数的返回值:%s' % page_content)
if __name__ == '__main__':
    # 创建一个协程对象
    c=get_urls('http://127.0.0.1:5000/index1')

    # 创建一个任务对象(基于已有的协程对象创建任务对象
    task=asyncio.ensure_future(c) # c值协程对象
    # 给该任务对象绑定一个回调函数
    task.add_done_callback(parse)

    # 创建一个事件循环对象
    loop=asyncio.get_event_loop()

    #将task存储到event_loop中且启动该对象
    loop.run_until_complete(task)

任务对象在事件循环中是否被异步的执行

一个任务对象在事件循环中无法检测出是否被异步执行
则需要将多个任务对象注册事件循环中进行测试
那么如何将多个任务对象注册到时间循环中呢?
Web爬虫 基于线程池的异步爬虫介绍 基于多任务协程爬虫介绍
import requests,asyncio,time

async def get_urls(url):
    print('get_urls被调用')
    page_content=requests.get(url).text #直接请求数据
    return len(page_content)

# 定义一个任务对象的回调函数
# 回调函数必须要要有一个参数 名字随意
# 该参数表示的就是该函数的绑定者 即任务对象task
def parse(task):
    # result():返回的就是特殊函数的返回值
    page_content=task.result()
    print('I am Task callback! 特殊函数的返回值:%s' % page_content)
if __name__ == '__main__':
    start=time.time()
    urls = ['http://127.0.0.1:5000/index1',
            'http://127.0.0.1:5000/index2',
            'http://127.0.0.1:5000/index3',
            ]
    #定义一个任务列表
    tasks=[]
    for url in urls:
        # 创建三个协程对象
        c = get_urls(url)
        # 创建三个任务对象
        task = asyncio.ensure_future(c)
        task.add_done_callback(parse) #绑定回调函数
        tasks.append(task)
    # 创建一个事件循环对象
    loop=asyncio.get_event_loop()

    # 将任务列表中的多个任务注册到了事件循环中
    loop.run_until_complete(tasks)
    # loop.run_until_complete(asyncio.wait(tasks))<
    print('总耗时:%s'% (time.time()-start))

以上代码只是测试,运行之后会报错:TypeError: An asyncio.Future, a coroutine or an awaitable is required,也就是我们不能将存在多个任务对象的tasks列表直接放到事件循环当中去,错误原因是在最后一句,应该这么写:loop.run_until_complete(asyncio.wait(tasks))

两者相比较,只是多了一个wait方法的调用,那么wait究竟表示什么呢?

wait方法的作用:
表示挂起的意思。asyncio.wait(tasks) 表示将任务列表中的每一个任务对象进行挂起。
挂起:表示让当前的任务对象交出CPU的使用权。
在计算机中没有真正的异步,是不可能将多个任务并发执行。就像你抬头看点灯一样,它一直在亮,但其实它是一直再闪,只不过频率比较高而已!异步也是如此的,它可以让CPU快速的切换,CPU执行每个任务,并且切换的频率比较高,看以来就相当于多个任务同时执行,但内部并不是同时执行的。多核CPU也是如此,其它内容不过多讨论!
Web爬虫 基于线程池的异步爬虫介绍 基于多任务协程爬虫介绍

修改代码后再次运行命令,你会发现这里的时间超过了9秒,代表它不是异步进行的。那么为什么呢?在这里需要注意一下几点:

在特殊函数内部的实现语句中不可以出现不支持一步的模块,否则会终断整个异步效果。
Web爬虫 基于线程池的异步爬虫介绍 基于多任务协程爬虫介绍

那么现在应该知道为什么代码不是异步进行了吧!我们的get_urls函数里面使用的是requests模块,它是不支持一步模块,所以它不应该出现在异步的代码里面,那么不使用requests模块的话,怎么发送请求呢?现在事大了,我只会requests和urllib发送请求呢该怎么办呢?

支持异步网络请求的模块【aiohttp】

aiohttp支持异步的发送网络请求,跟requests和urllib模块差不多,可以发送get和post请求,只不过它支持异步。想要使用就需要使用命令pip install aiohttp进行安装。官方建议我们使用with..as语句,实例化某个资源可以自动关闭,不用手动关闭。那么我们现在就需要修改get_urls函数,如何修改呢?一起来看一下aiohttp的使用,先编写一个大致的架构:

# 特殊函数: 不可以出现不支持异步模块的代码,不可以使用rquests请求模块
async def get_urls(url):
    with aiohttp.ClientSession() as session:  # 实例化一个请求对象叫做session
        # session.get(url,headers,params,proxy)
        # session.post(url,headers,data,proxy
        # requests模块接收proxies 格式为字典
        # 这里的session.get接受proxy 格式为"http://ip:port"
        with session.get(url=url) as response: # 调用get请求,返回一个response
            page_content=response.text() # 获取了页面HTML数据 返回的字符串不需要关闭
            # text()返回字符串性质的响应数据
            # read()返回bytes类型的响应数据
            return page_content

这里不要运行代码,因为会报错,细节还没有完善,接下来需要在架构中补充一下。在每一个with前加上async关键字,在每一个阻塞操作前加上await(等待),什么会阻塞呢?就是请求和数据响应部分,那么修改代码:

import asyncio,time
import aiohttp
# 特殊函数: 不可以出现不支持异步模块的代码,不可以使用rquests请求模块
async def get_urls(url):
     async with aiohttp.ClientSession() as session:  # 实例化一个请求对象叫做session
        # session.get(url,headers,params,proxy)
        # session.post(url,headers,data,proxy
        # requests模块接收proxies 格式为字典
        # 这里的session.get接受proxy 格式为"http://ip:port"
        async with await session.get(url=url) as response:
            # 调用get请求,返回一个response
            page_content= await response.text() # 获取了页面HTML数据 返回的字符串不需要关闭
            # text()返回字符串性质的响应数据
            # read()返回bytes类型的响应数据
            return page_content

现在重新运行程序,记得打开flask服务端,其它内容同上。你会发现时间在3秒左右,就实现了异步爬虫,对于await的含义,你需要知道:

await关键字:可以确保在异步执行操作的过程中可以保证阻塞操作执行完毕。如果阻塞操作前没有await,在执行操作的时候一旦遇到阻塞,就不会等待,直接执行下一步;如果加上await,表示遇到阻塞会一直等待,直到阻塞完成,等待的时间里会把CPU的使用权先交出来,执行下一个其它的操作!
Web爬虫 基于线程池的异步爬虫介绍 基于多任务协程爬虫介绍

多任务的异步爬虫中数据解析或者持久化存储操作需要写在任务对象的回调函数中

多任务实战说明

如果想使用该模式进行异步的数据爬取规则,必须将等待即将被爬取的页面的url单独的抽取并存储到一个列表中。通常情况下的做法就是先使用requests将等待爬取的页面的url获取到,然后将url写入到列表,使用多任务异步协程爬取列表中的页面数据。

未经允许不得转载:作者:鳄鱼君, 转载或复制请以 超链接形式 并注明出处 鳄鱼君
原文地址:《Web爬虫 基于线程池的异步爬虫介绍 基于多任务协程爬虫介绍》 发布于2020-05-07

分享到:
赞(0) 赏杯咖啡

评论 抢沙发

9 + 8 =


文章对你有帮助可赏作者一杯咖啡

支付宝扫一扫打赏

微信扫一扫打赏

Vieu4.6主题
专业打造轻量级个人企业风格博客主题!专注于前端开发,全站响应式布局自适应模板。
切换注册

登录

忘记密码 ?

您也可以使用第三方帐号快捷登录

Q Q 登 录
微 博 登 录
切换登录

注册