Bootstrap

爬虫学习笔记(第六章)高性能异步爬虫


前言

2021.08.01协程听得有点蒙,弄完第六章。后面还差一点,一会再补上,过了12点就2号了。
2021.08.02补充了代码实现中的⑥⑦。


第六章

1.知识点

高性能异步爬虫

  • 目的:在爬虫中使用异步实现高性能的数据爬取操作。
  • 异步爬虫的方式:
    • 1.多线程、多进程(不建议):
      • 好处:可以为相关阻塞的操作单独开启线程或进程,阻塞操作就可以异步执行。
      • 弊端:无法无限制的开启多线程或者多进程。
    • 2.线程池、进程池(适当的使用):
      • 好处:我们可以降低系统对进程或者线程创建和销毁的频率,从而很好地降低系统的开销。
      • 弊端:池中线程或进程的数量是有上限的。
      • 原则:线程池处理的是阻塞且较为耗时的操作
    • 3.单线程 + 异步协程(推荐):
      • 一些概念和两个关键字:
        • event_loop事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足某些条件时,函数就会被循环执行。
        • coroutline协程对象,我们可以将协程对象注册到事件循环中,它会被事件循环调用。我们可以使用async关键字来定义一个方法,这个方法在调用时不会被立即被执行,而是返回一个协程对象。
        • task任务,它是对协程对象的进一步封装,包含了任务的各个状态。
        • future:代表将来执行或还没有执行的任务,实际上和task没有本质区别。
        • async:定义一个协程。
        • await:用来挂起阻塞方法的执行。

2.代码实现

①单线程爬取数据

使用单线程爬取数据案例展示:

import requests

headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
    }
def get_content(url):
    print('正在爬取:', url)
    # get方法是一个阻塞的方法
    response = requests.get(url=url, headers=headers)
    print(response.status_code)
    if response.status_code == 200:
        return response.content

def parse_content(content):
    print('相应数据的长度为:', len(content))

if __name__ == "__main__":
    urls = [
            'https://downsc.chinaz.net/Files/DownLoad/jianli/202107/jianli15646.rar',
            'https://downsc.chinaz.net/Files/DownLoad/jianli/202107/jianli15647.rar',
            'https://downsc.chinaz.net/Files/DownLoad/jianli/202107/jianli15651.rar',
        ]
    for url in urls:
        content = get_content(url)
        parse_content(content)

②线程池爬取数据

使用线程池爬取数据案例展示:

# import time
#
#
# # 使用单线程串行方式执行
#
# def get_page(str):
#     print("正在下载:", str)
#     time.sleep(2)
#     print("下载成功", str)
#
#
# if __name__ == "__main__":
#     name_list = ['xiaozi', 'aa', 'bb', 'cc']
#
#     start_time = time.time()
#
#     for i in range(len(name_list)):
#         get_page(name_list[i])
#
#     end_time = time.time()
#     print('%d second' % (end_time - start_time))


import time
# 导入线程池对应的类
from multiprocessing.dummy import Pool
# 使用线程池方式执行


def get_page(str):
    print("正在下载:", str)
    time.sleep(2)
    print("下载成功", str)


if __name__ == "__main__":
    start_time = time.time()
    name_list = ['xiaozi', 'aa', 'bb', 'cc']
    # 实例化一个线程池对象
    # (优化后耗时2秒,试了一下,把4改成3耗时4秒,推测是有一条为单线)
    pool = Pool(4)
    # 将列表中每一个列表元素传递到gert_page进行处理
    # map的返回值是get_page函数的返回值,为一个列表,此处不需要进行处理
    pool.map(get_page, name_list)

    end_time = time.time()
    print('%d second' % (end_time - start_time))

③爬虫中应用线程池(动态加载的video标签待解决)

爬虫中应用线程池:

import requests
from lxml import html
etree = html.etree
import re
from multiprocessing.dummy import Pool

# 需求:爬取视频的视频数据
headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
    }
# 原则:线程池处理的是阻塞且较为耗时的操作


def get_video_data(dic):
    url = dic['url']
    print(dic['name'], 'downloading...')
    data = requests.get(url=url, headers=headers).content
    # 持久化存储操作
    with open(dic['name'], 'wb') as fp:
        fp.write(data)
        print(dic['name'], '下载成功')


if __name__ == "__main__":
    # 对下述url发起请求解析出视频详情页的url和视频名称
    url = 'https://www.pearvideo.com/category_5'
    page_text = requests.get(url=url, headers=headers).text

    tree = etree.HTML(page_text)
    li_list = tree.xpath('//ul[@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)
        # 对详情页的url发起请求
        detail_page_text = requests.get(url=detail_url, headers=headers).text
        # 从详情页中解析出视频的地址(url)

        # 用作案例的网站在教程中的那个时间段(一几年,大概是18年)
        # 没有video标签,视频的地址在js里面,所以不能用bs4和xpath
        # 但是网站已经更新(2021.08.01),我现在学的时候src的Url已经在video标签中了
        # 因此此处正则不再适用,咱用xpath修改一下

        # 教程代码:
        # ex = 'srcUrl="(.*?)",vdoUrl'
        # video_url = re.findall(ex, detail_page_text)[0]

        # 咱的代码:
        # detail_tree = etree.HTML(detail_page_text)
        # print(detail_page_text)
        # video = detail_tree.xpath('//*[@id="JprismPlayer"]/video')
        # print(video)
        # video_url = video[0]

        # 哈哈,放video标签之后不知道是人家反爬了还是咱的爬取有问题
        # 应该是弄得Ajax动态加载
        # 爬到的:
        # <div class="main-video-box" id="drag_target1">
        #     <div class="img prism-player" style="height:100% !important;" id="JprismPlayer">
        #     </div>
        # </div>

        # F12看到的:
        # <div class="main-video-box" id="drag_target1">
        #     <div class="img prism-player play" style="height: 100% !important; width: 100%;" id="JprismPlayer"
        #     x-webkit-airplay="" playsinline="" webkit-playsinline="" >
        #         < video webkit-playsinline="" playsinline="" x-webkit-airplay="" autoplay="autoplay"
        #         src="https://video.pearvideo.com/mp4/third/20210801/cont-1737209-12785353-091735-hd.mp4"
        #         style="width: 100%; height: 100%;">
        #         < / video >
        #         ...
        #     </div>
        # </div>
        # 又挖了一个坑,以后再填qwq
        video_url = ''
        dic = {
            'name': name,
            'url': video_url
        }
        urls.append(video_url)
        # 使用线程池对视频数据进行请求(较为耗时的阻塞操作)
        pool = Pool(4)
        pool.map(get_video_data, urls)

        pool.close()
        pool.join()

④协程

代码如下:

import asyncio


async def request(url):
    print('正在请求的url是', url)
    print('请求成功', url)
    return url


# async修饰的函数,调用之后返回的一个协程对象
c = request('www.baidu.com')


# # 创建一个事件循环对象
# loop = asyncio.get_event_loop()
#
# # 将协程对象注册到loop中,然后启动loop
# loop.run_until_complete(c)

# # task的使用
# loop = asyncio.get_event_loop()
# # 基于loop创建task任务对象
# task = loop.create_task(c)
# print(task)
#
# loop.run_until_complete(task)
#
# print(task)

# # future的使用
# loop = asyncio.get_event_loop()
# task = asyncio.ensure_future(c)
# print(task)
# loop.run_until_complete(task)
# print(task)


def callback_func(task):
    # result返回的是任务对象中封装的协程对象对应函数的返回值
    print(task.result())


# 绑定回调
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(c)
# 将回调函数绑定到任务对象中
task.add_done_callback(callback_func)
loop.run_until_complete(task)

⑤多任务异步协程01

代码如下:

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.goubanjia.com'
]

# 任务列表:存放多个任务对象
stasks = []
for url in urls:
    c = request(url)
    task = asyncio.ensure_future(c)
    stasks.append(task)

loop = asyncio.get_event_loop()
# 需要将任务列表封装到wait中
loop.run_until_complete(asyncio.wait(stasks))

print(time.time()-start)

⑥多任务异步协程02

搭建一个简单的web服务器:

from flask import Flask
import time

app = Flask(__name__)


@app.route('/john')
def index_john():
    time.sleep(2)
    return 'Hello john'


@app.route('/smith')
def index_smith():
    time.sleep(2)
    return 'Hello smith'


@app.route('/tom')
def index_tom():
    time.sleep(2)
    return 'Hello tom'


if __name__ == "__main__":
    app.run(threaded=True)

异步协程实现代码(此处仍为单线):

import requests
import asyncio
import time


start = time.time()
urls = [
    'http://127.0.0.1:5000/john',
    'http://127.0.0.1:5000/smith',
    'http://127.0.0.1:5000/tom',
]


async def get_page(url):
    print('正在下载', url)
    # requests.get是基于同步的,必须使用基于异步的网络请求模块进行指定url的请求发送
    # aiohttp:基于异步网络请求的模块
    response = requests.get(url)
    print('下载完毕', response.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)

⑦aiohttp实现多任务异步协程

  • aiohttp:基于异步网络请求的模块
  • 环境安装:pip install aiohttp
  • 使用环境中的ClientSession
  • 与requests模块的区别,调用的不是属性,而是方法:
    • text()返回字符串形式的响应数据
    • read()返回二进制形式的响应数据
    • json()返回的是json对象

代码如下:

import requests
import asyncio
import time
import aiohttp
# 环境安装:pip install aiohttp
# 使用环境中的ClientSession
start = time.time()
urls = [
    'http://127.0.0.1:5000/john',
    'http://127.0.0.1:5000/smith',
    'http://127.0.0.1:5000/tom',
]


async def get_page(url):
    async with aiohttp.ClientSession() as session:
        # 使用不同的请求类型:
        # get()、post()
        # 添加相关的参数:
        # headers,params/data proxy='http://ip:port'
        # 例如:
        # async with await session.get(url, params, proxy) as response:
        # async with await session.post(url, data, proxy) as response:
        async with await session.get(url) as response:
            # text()返回字符串形式的响应数据
            # read()返回二进制形式的响应数据
            # json()返回的是json对象
            # 注意: 获取响应数据操作之前一定要使用await进行手动挂起
            page_text = await response.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)
;