异步提升网页采集速度

73 阅读4分钟

本篇为学习笔记,内容来自于崔庆才的《Python3 网络爬虫开发实战 第二版》,如果内容涉及侵权,请联系我进行删除,同时,在编写过程中也存在不足之处,还请各位帮忙指正。相关代码模块在码云

目录

用途

> 该模块主要用来处理一些常见的采集任务,比如有几百个网页需要采集,可以使用本模块来提升采集速度

速度对比

使用requests进行采集

# 使用requests进行同步采集
import requests
import time
header = {
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
    }
urls = [f"https://spa5.scrape.center/api/book/?limit=18&offset={i * 18}" for i in range(0, 500)]
responses = []
start_time = time.time()
for url in urls:
    print(url)
    res = requests.get(url, headers=header)
    responses.append(res.json())
for res in responses:
    print(res)
print("共计用时:", time.time() - start_time)
共计用时: 117.82907366752625

使用模块进行采集

from asyRequest import AsyRequest
import time
urls = [f"https://spa5.scrape.center/api/book/?limit=18&offset={i * 18}" for i in range(0, 500)]
start_time = time.time()
A = AsyRequest()
A.PutRequests(urls)
A.start()
responses = A.GetResponses()
for res in responses:
    print(res.json())
print("共计用时:", time.time() - start_time)
共计用时: 77.28473448753357

模块构造函数

> 在实例化模块的时候,需要提供两个参数,`CONCURRNCY`和`TimeInterval`,其中,`CONCURRNCY`代表的是在一个周期内可以并行的协程数量,这个数值越大,采集速度越快(也不要特别大,可能会对网站造成不好的影响,且特别大的时候,也不一定能更快),`TimeInterval`则代表着暂停时间,这个参数数值越大,采集速度越慢,但是对网站影响越小。

传入采集网站列表

首先在构造函数中,我们初始化了两个队列,分别是`request_qneue`和`response_qneue`,其中`request_qneue`用来保存我们的请求内容,为了能够方便我们更好的构造请求,提供了两个对外的api接口,分别是`PutRequests`和`PutMethodRequest`。

PutRequests

接收的是一个包含需要采集的所有网站链接的列表内容,作用是将这些网站链接自动封装为简单的GET请求(如果存在POST请求链接,则这个方法就行不通了,且不能个性化设计采集的其他参数),但是这种方法更加方便、快捷。

PutMethodRequest

接收的是一个个性化的链接采集配置,可以自行设置各项参数,然后将这个链接加入到采集队列中,缺点是需要一个链接一个链接的设置。

启动函数

当我们设置好采集队列以后,我们可以通过`实例.start()`启动模块进行采集。

获取采集到的响应列表

模块会将采集到的链接保存到`response_qneue`队列中去,我们可以通过`实例.GetResponses获取到它`

用到的asyncio的API描述

asyncio.run

运行一个协程函数一般用async装饰的函数我们就称为协程函数,并等待其返回结果。
例如下面这样:

import asyncio
async def runner():
    print("hello")
    await asyncio.sleep(10)  # 阻塞代码,如果存在多个协程的时候,将切换到其他协程上
    print("world")
    return 10
asyncio.run(runner())

asyncio.Semaphore

用来限制协程的并发数量,需要保证这个对象是全局对象

asyncio.gather

并发执行多个协程,且会等待多个协程执行完成并提取各个协程函数的返回值

完整模块代码

import asyncio
import aiohttp
class CustomResponse:
    def __init__(self, *, url=None, status_code=404, text=None, content=None, jsp=None):
        self.url = url
        self.text = text
        self.status_code = status_code
        self.content = content
        self.jsp = jsp

    def json(self):
        return self.jsp

class AsyRequest:
    header = {
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
    }
    def __init__(self, CONCURRNCY=10, TimeInterval=-1):
        """
        :param CONCURRNCY: 周期内的并发数量
        :param TimeInterval: 周期 单位为秒(小于0为不设置周期)
        """
        self.Interval = TimeInterval  # 时间间隔
        self.sem = asyncio.Semaphore(CONCURRNCY)
        self.request_qneue = []  # 保存封装的请求
        self.response_qneue = []  # 保存封装的响应
        self.session = None

    def PutRequests(self, urls:list):
        for url in urls:
            self.request_qneue.append(
                {
                    "url":url,
                    "method":"GET",
                    "headers":self.header,
                    'kwargs':{}
                }
            )

    def PutMethodRequest(self, url, *, method="GET", headers=None, **kwargs):
        self.request_qneue.append(
            {
                "url": url,
                "method": "GET" if method == "GET" else "POST",
                "headers": headers if headers else self.header,
                'kwargs': kwargs
            }
        )

    async def Response(self, response: aiohttp.ClientResponse):
        status_code = response.status
        url = response.url
        text = await response.text()
        content = await response.read()
        json_data = await response.json()
        return CustomResponse(url=url, status_code=status_code, text=text, content=content, jsp=json_data)

    async def Request(self, item):
        async with self.sem:
            print(item['url'])
            url = item['url']
            headers = item['headers']
            method = item['method']
            kwargs = item.get('kwargs', {})  # 确保 kwargs 是一个字典
            if self.Interval > 0:
                await asyncio.sleep(self.Interval)
            if method == "GET":
                async with self.session.get(url, headers=headers, **kwargs) as response:
                    Res = await self.Response(response)
            else:
                async with self.session.post(url, headers=headers, **kwargs) as response:
                    Res = await self.Response(response)
        return Res

    async def run(self):
        async with aiohttp.ClientSession() as session:
            self.session = session
            tasks = [self.Request(item) for item in self.request_qneue]
            self.request_qneue = []
            self.response_qneue = await asyncio.gather(*tasks)

    def start(self):
        asyncio.run(self.run())

    def GetResponses(self):
        return self.response_qneue