异步爬虫

异步爬虫

发起一个请求,不用等待它按序操作结束,当它结束时会通知。实现方式是通过单线程+协程实现异步IO操作。

相关概念

同步与异步

同步:不同程序为了完成任务,在执行过程中需要靠某种通信方式以协调一致,被称为同步执行。
异步:不同程序为了完成任务,各任务之间过程中无需通信协调也能完成,被称为异步执行。

可理解为同步是串行,按照ABCD顺序执行;异步是并行,ABCD同时进行

阻塞与非阻塞

阻塞:程序在等待某个操作完成期间,无法分身执行别的任务,被称为阻塞
非阻塞:程序在执行操作时,不能立刻得到结果之前,该调用不会阻塞当前线程

基于线程池

个人简单理解就是,线程多了,放在一个池里方便管理,于是就有了线程池。

搭建模拟网站

基于Flask框架

  • 在templates文件夹里创建一个HTML,把它作为模拟网站
  • Mark directory as template folder
  • 实例化一个app,开启调试模式
  • 创建视图函数&路由地址
  • 运行(只保存不运行服务器未开启)
from flask import Flask,render_template
from time import sleep

# 实例化一个App
app = Flask(__name__)

# 创建视图函数&路由地址
@app.route('/banban')
def index_01():
    sleep(2)
    return render_template('shiwen.html')
@app.route('/shoushou')
def index_02():
    sleep(2)
    return render_template('shiwen.html')
@app.route('/huhu')
def index_03():
    sleep(2)
    return render_template('shiwen.html')



if __name__ == '__main__':
    # debug=True 表示开启调试模式:服务器端代码被修改后按下保存进会自动重启服务
    app.run(debug=True)

在这里插入图片描述

线程池

注:该处存疑,multiprocessing.dummy.pool有认为是线程池有认为是进程池,在这里暂且把它当做是线程池

multiprocessing.dummy.Pool.map(func,iterable [,chunksize ] )
此方法将iterable内的每一个对象作为单独的任务提交给线程池,map()支持1个可迭代的参数,如果需要多参数,可以将func作为键值对写入。
在这里,可以使用func(callback)对iterable(alist)列表中的每一个元素进行指定形式的异步操作,map()返回值是一个列表。如果存在1个以上的map(),map()里是异步操作,map()与map()间是串行(按顺序执行)的。

线程池

  • 将请求的网站封装
  • 定义一个访问类
  • 在同步里,按顺序一个接一个执行
  • 在异步里,同时执行(乱序执行)
import requests
import time

from multiprocessing.dummy import Pool
# multiprocessing.dummy 多线程
# 非阻塞方法:multiprocessing.dummy.Pool.apply_async() 和 multiprocessing.dummy.Pool.imap()
# 阻塞方法:multiprocessing.dummy.Pool.apply()和 multiprocessing.dummy.Pool.map()
# multiprocessing.dummy.Pool.apply_async()可以有返回值,apply(),map(),imap()不可以设置返回值

urls = [
    'http://127.0.0.1:5000/banban',
    'http://127.0.0.1:5000/shoushou',
    'http://127.0.0.1:5000/huhu'
]

def get_request(url):
    page_text = requests.get(url=url).text
    return len(page_text)

# 同步代码
# if __name__ == '__main__':
#     start = time.time()
#
#     for url in urls:
#         res = get_request(url)
#         print(res)
#
#     print('total_time:',time.time()-start)


# 异步代码
if __name__ == '__main__':
    start = time.time()
    # Pool(3)表示开启线程的数量
    pool = Pool(3)
    # 使用get_request作为回调函数,需要基于异步的形式对urls列表中的每一个列元素进行操作
    # 保证回调函数必须要有一个参数和返回值
    result_list = pool.map(get_request, urls)
    print(result_list)
    print('total_time:',time.time()-start)

同步耗时6秒,即一个接一个的执行程序
在这里插入图片描述

异步耗时2秒,多任务封装在线程里,同时执行(乱序执行)
在这里插入图片描述

单线程+多任务异步协程

一些概念

特殊的函数

如果一个函数的定义被async修饰后,该函数就变为一个特殊函数。
特点

  • async是asyncio模块特有的关键字,不导入无法使用
  • 该特殊函数调用后,函数内部的实现语句不会被立即执行
  • 该特殊函数被调用后会返回一个协程对象
协程对象

coroutine
特殊函数被调用后会返回一个协程对象,协程对象需要注册到事件循环,由事件循环调用
协程 == 特殊函数 == 一组指定操作
协程 == 一组指定操作

任务对象

task
任务对象是一个高级的协程对象,它是对协程对象的进一步封装,其中包含了任务的各种状态(可挂起)
任务 == 协程 == 特殊函数 == 一组指定操作
任务 == 协程 == 一组指定操作

创建任务对象
asyncio.ensure_future(协程对象)

任务对象的高级之处

  • 可以给任务对象绑定回调 task.add_done_callback(task_callback)
  • 回调函数的调用时机是任务被执行结束后
  • 回调函数的参数只可以有一个,就是该回调函数的调用者(任务对象)
  • 使用回调函数的参数调用result()返回的就是任务对象表示的特殊函数return的结果
事件循环对象

event_loop
首先,它是一个对象。

作用

  • 可以将多个任务对象注册/装载到事件循环对象中
  • 如果开启了循环,则其内部注册/装载的任务对象表示的指定操作就会被基于异步的被执行

创建方式
loop = asyncio.get_event_loop()

注册/启动

loop.run_until_complete(task)

wait()

作用:将任务列表中的任务对象附于可被挂起的权限,只有任务对象被赋予这个权限后才能被挂起。
挂起:将当前的任务对象交出cpu的使用权。

await关键字

在特殊函数内部,凡是阻塞操作前都必须使用await进行修饰,可以保证阻塞操作在异步执行过程中不会被跳过。

注意:在特殊函数内部不可以出现不支持异步模块对应的代码,否则会中断整个异步效果。

aiohttp

它是一个支持异步的网络请求模块。

可以在anaconda-environments-base(root)里查找是否存在该模块
在这里插入图片描述

如果没有,则全局搜索后直接下载即可
在这里插入图片描述

一些测试

注册/开启事件循环

import asyncio
import requests
import time

async def get_request(url):
    print('正在请求的url:',url)
    time.sleep(2)
    print('请求结束:',url)
    return 'banban'

# 回调函数的封装
# 参数t:任务对象 -- 也就是该回调函数的调用者
def task_callback(t):
    print('i`m task_callback(),参数是:',t)
    # return的是协程对象(任务对象)的返回值
    print('t.result()返回的是:',t.result())

if __name__ == '__main__':
    # 随机访问1个网站,c就是一个协程对象
    c = get_request('www.1.com')
    # 任务对象就是对协程对象的进一步封装
    task = asyncio.ensure_future(c)
    # 给任务对象task 绑定一个回调对象
    task.add_done_callback(task_callback)
    # 创建事件循环对象
    loop = asyncio.get_event_loop()
    # 将任务对象(协程对象的封装)注册到事件循环对象中并开启事件循环
    loop.run_until_complete(task)

在这里插入图片描述

多任务(没有异步效果)

import time
import requests
import asyncio
'''
在特殊函数内部不可以出现不支持异步模块对应的代码,否则会中断整个异步效果
在特殊函数内部,凡是阻塞操作前都必须使用await进行修饰。await就可以保证阻塞操作在异步执行的过程中不会被跳过
'''

async def get_request(url):
    print('正在请求的url:',url)
    # time.sleep(2) #不支持异步模块的代码
    await asyncio.sleep(2) # 支持异步模块的代码
    print('请求结束:',url)
    return 'banban'

urls = [
    'www.1.com',
    'www.2.com',
    'www.3.com'
]

if __name__ == '__main__':
    start = time.time()
    tasks = []

    for url in urls:
       c =  get_request(url) #创建协程对象
       task = asyncio.ensure_future(c) #封装为任务对象
       tasks.append(task) #加入多任务列表
    # 创建事件循环对象
    loop = asyncio.get_event_loop()
    # 使用asyncio.wait()封装多任务列表后,注册并开启事件循环,wait可以将任务列表中的任务对象进行挂起操作
    loop.run_until_complete(asyncio.wait(tasks))
    print('total_time:',time.time()-start)

在这里插入图片描述

多任务的异步爬虫
使用多任务的异步协程爬取数据实现套路:

  • 可以先使用requests模块将待请求数据对应的url封装到有个列表中(同步)
  • 可以使用aiohttp模式将列表中的url进行异步的请求和数据解析(异步)
import requests
import time
import asyncio
import aiohttp

from lxml import etree

urls =[
    'http://127.0.0.1:5000/banban',
    'http://127.0.0.1:5000/shoushou',
    'http://127.0.0.1:5000/huhu'
]

# async def get_request(url):
#     #requests是一个不支持异步的模块
#     page_text = requests.get(url).text
#     return page_text

async def get_request(url):
    #实例化1个请求对象 注意with as写法,且要async修饰
    async with aiohttp.ClientSession() as sess:
        #调用get请求返回1个响应对象
        # requests与aiohttp的get/post(url, headers,params/data一致,aiohttp是proxy="字符串 http://ip:port",requests是proxies)
        async with await sess.get(url=url) as response:
            # text()获取了字符串形式的响应数据
            # read()获取byte类型的响应数据
            page_text = await response.text()
            return page_text

# 解析函数的封装,使用任务对象的回调函数实现数据解析
# 多任务的架构中数据的爬取是封装在特殊函数中,我们一定要保证数据请求结束后,在实现数据解析。
def parse(t):
    # 特殊函数返回的是页面源码数据
    page_text = t.result()
    tree = etree.HTML(page_text)
    parse_text = tree.xpath('//a[@id="feng"]/text()')[0]
    print(parse_text)


if __name__ == '__main__':
    start = time.time()
    tasks = []
    for url in urls:
        #创建协程对象
        c = get_request(url)
        #将协程对象封装为任务对象
        task = asyncio.ensure_future(c)
        #给任务对象绑定一个回调对象
        task.add_done_callback(parse)
        #将任务对象添加到多任务列表
        tasks.append(task)
    # 创建事件循环对象
    loop = asyncio.get_event_loop()
    # 使用asyncio.wait()封装多任务列表后,注册并开启事件循环
    loop.run_until_complete(asyncio.wait(tasks))
    print('total_time:',time.time()-start)

在这里插入图片描述

在写代码时还是常犯几个错误,url里各地址间没有加, 导致没有加载,loop没有跳出循环,没有做出异步效果,还需要不断改正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值