异步爬虫简单使用

一.异步爬虫的理解

        在我们利用爬虫去大量爬取资源的时候,爬虫的效率是我们非常关心的一件事情。比如这里有1000个文件需要抓取,如果使用常规的单线程去抓取那么效率将会非常缓慢,所以我们可以考虑去使用多线程,或者开线程池去爬取。但是python里面的多线程也有很多弊端,比如说:

  1. GIL(全局解释器锁): 在使用 CPython 解释器时,由于GIL的存在,多线程并不能真正实现并行执行。GIL会限制同一时刻只有一个线程执行Python字节码,从而影响多线程并发效果。

  2. 竞态条件和同步问题: 多线程会引入竞态条件和同步问题,可能导致数据不一致或者产生难以调试的 bug。需要使用锁、信号量等机制来解决这些问题。

  3. 线程切换开销: 线程切换也会带来一定的开销,尤其是在线程数较多的情况下。如果线程数过多,可能会导致操作系统频繁切换,影响性能。

  4. 资源消耗: 多线程爬虫可能会消耗较多的系统资源,尤其是在创建大量线程的情况下。这可能导致内存占用增加,对系统造成负担。

所以我们有必要去了解一种全新的爬虫方式--异步爬虫。那么为什么异步爬虫的效率会很高?原因可以分为以下几点:

  1. 并发执行: 异步爬虫允许在等待某个IO操作完成的同时执行其他任务。这样可以充分利用系统资源,实现并发处理多个请求,从而提高整体效率。

  2. 非阻塞IO操作: 异步爬虫使用非阻塞的IO操作,意味着当发起一个IO请求时,爬虫不会等待操作完成,而是可以继续执行其他任务。这样可以最大限度地减少等待时间,提高IO利用率。

  3. 轻量级协程: 在异步编程中,任务被组织为轻量级的协程。协程相比线程更为轻量,创建和销毁的成本较低,可以更灵活地管理大量任务。

  4. 事件循环: 异步爬虫通过事件循环管理任务的执行顺序,使得任务之间的切换更为高效。事件循环负责检查并处理发生的事件,包括IO操作完成、定时器触发等。

  5. 避免线程切换开销: 相比多线程爬虫,异步爬虫避免了线程切换的开销。线程切换涉及到上下文切换和资源共享的开销,而协程的切换更为轻量,不需要涉及到线程切换的复杂性。

二.准备工作

        前面说了这么多,看起来异步爬虫似乎比多线程爬虫要复杂一些,事实也确实是这样的。但是毕竟我们不用去完全搞清楚它底层是怎么运行的,所以只要掌握一些使用的套路就可以了。我们需要的关键东西有aiofiles、asyncio、aiohttp。其中第一个是支持异步文件操作的库,第二个是使用异步操作,创建协程或者说异步函数的库,第三个是支持异步操作的请求库。在异步爬虫里面,requests库是无法使用的。

三.确定爬取目标

        既然是爬虫,我们就要选定一个爬取的目标,这里我们就选定某个网站上的《你的名字》这部电影吧,因为这部电影被分为了一千六百多个ts片段,所以也是一个比较大的爬取任务。

def getTs():
    """
    该函数是用来获取所以视频片段的地址
    :return:
    """
    response=requests.get(url).text
    pattern=re.compile(r"c997993.*?ts")
    result=pattern.findall(response)
    for i in result:
        tslist.append(i)

这个函数是我们用来获取所有ts片段的地址的,并将其保存到一个列表里面,使用了requests、正则表达式,这个函数并没有异步操作。至于怎么找到这些地址的,大家可以去看其他大佬的文章或者是我的另外一篇作文。

四.发送异步请求下载资源

        接下来我们就需要写一个异步函数了,这个函数的参数是传入一个地址,然后进行下载并保存到本地。

async def downLoad(url):
    name=url.replace('.ts','')
    url="https://vip.lz-cdn2.com/20220320/142_f4aff3f7/1200k/hls/"+url
    async with semaphore:
        #async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
            try:
                async with session.get(url,headers=header) as response:
                    content = await response.read()

            except:
                for i in range(10):
                    try:
                        async with session.get(url,headers=header) as response:
                            if response.status == 200:
                                content = await response.read()
                                async with aiofiles.open(f"{name}.ts", 'wb') as f:
                                    await f.write(content)
                                    tsName.append(f"{name}.ts")
                                    bar.update()
                                break
                    except:
                        pass
            else:
                async with aiofiles.open(f"{name}.ts", 'wb') as f:
                    await f.write(content)
                    tsName.append(f"{name}.ts")
                    bar.update()

让我来详细解释一下。函数内部的前两行大家可以不用管,这只是对传入的地址进行了一些操作,方便后面使用而已。这个函数定义的时候需要使用关键字async,表明它是一个异步函数或者说协程。有异步操作的with语句我们也需要用async with来代替。函数第三行有一个semaphore,它是我在函数外定义的一个最大并发量,因为异步爬虫支持的并发量非常高,可以达到百万级别,如果不加以限制的话,对方的服务器很可能不能处理如此高的并发,导致响应缓慢甚至寄掉。所以在函数外面我这样定义了一个并发量上限 ,并用async with将用于下载的代码包裹住。

#定义最大并发量
semaphore=asyncio.Semaphore(50)

紧接着我使用了一些try和except语句用来处理异常情况,这个大家不用在意。然后是使用aiohttp库请求的代码

                async with session.get(url,headers=header) as response:
                    content = await response.read()

这里同样要使用async with语句,然后使用session.get(url,headers=header)发送请求,这里的session是一个异步请求的会话,同样它也是我在另外的地方里面定义好的,

session=aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20))

并且定了一个超时的限制,一旦超时就会抛出一个异常被捕获。content=await response.read()这行代码是一个关键,await关键字表示挂起,也就是说response.read没有马上响应的话,就会去执行别的协程,等到它响应再回来继续执行。后面的异步写文件操作也是同样的道理。

五.管理所有异步任务

        前面我们定义了处理一个下载任务的异步函数,但是我们的下载任务有一千多个,所以我们还需要一个异步函数来管理他们。

async def main():
    global session
    session=aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20))
    task=[]
    for i in tslist:
        task.append(asyncio.ensure_future(downLoad(i)))
    result=await asyncio.gather(*task)
    await session.close()

这个函数我们能看得很清楚了,先是定义了一个会话,然后定义了一个任务列表,把所有执行任务的协程放进去。asyncio.ensure_future 是用于将一个协程任务(coroutine)包装成一个 Task 对象的函数。在异步编程中,Task 是一个在事件循环中可以被调度执行的对象。ensure_future 的作用是确保一个协程任务被正确地包装成一个 Task 对象,以便能够在事件循环中进行调度。这里我们虽然调用了下载函数,但是它并没有开始执行。然后我们使用了asyncio.gather 是用于同时运行多个协程任务(coroutines)的函数。它接受多个协程作为参数,并在事件循环中并发执行这些协程,等待它们全部完成后返回结果(一个列表)给result。最后我们要关闭会话,同样要使用await关键字。

六.启动任务

        前面我们定义好了main函数,接下来只需要调用它执行就可以,启动的方式也是比较特别的

asyncio.get_event_loop().run_until_complete(main())

协程任务的启动需要依托于事件循环,所以我们用get_event_loop创建了事件循环,然后使用run_until_complete函数真正启动任务。

七.其他不太重要的函数

        第一个是合并所有ts文件为mp4的函数

def merge_ts_files(ts_files, output_file):
    # 将TS文件列表写入文本文件,因为命令行长度的限制,这里要合并的文件太多
    ts_files.sort()
    with open('ts_files_path.txt', 'w') as file:
        for i in ts_files:
            file.write(f"file '{i}'\n")
    # 使用FFmpeg命令引用文件列表
    cmd = ['ffmpeg', '-f', 'concat', '-safe', '0', '-i', 'ts_files_path.txt', '-c', 'copy', output_file]

    # 调用FFmpeg命令
    subprocess.call(cmd)

第二个是合并完成后删除所有ts文件的函数

def removeTs():
    """
    删除所有ts文件
    :return:
    """
    current_directory=os.getcwd()
    files=os.listdir(current_directory)

    for file in files:
        if file.endswith(".ts"):
            filepath=os.path.join(current_directory,file)
            os.remove(filepath)

八.完整代码

import aiofiles
import asyncio
import aiohttp
import re
import requests
import os
from tqdm import tqdm
import time
import subprocess
header={
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0"
}
url="https://vip.lz-cdn2.com//20220320//142_f4aff3f7//1200k/hls/mixed.m3u8"
tslist=[]
tsName=[]
#定义最大并发量
semaphore=asyncio.Semaphore(50)
#创建会话
session=None
def getTs():
    """
    该函数是用来获取所以视频片段的地址
    :return:
    """
    response=requests.get(url).text
    pattern=re.compile(r"c997993.*?ts")
    result=pattern.findall(response)
    for i in result:
        tslist.append(i)

async def downLoad(url):
    name=url.replace('.ts','')
    url="https://vip.lz-cdn2.com/20220320/142_f4aff3f7/1200k/hls/"+url
    async with semaphore:
        #async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
            try:
                async with session.get(url,headers=header) as response:
                    content = await response.read()

            except:
                for i in range(10):
                    try:
                        async with session.get(url,headers=header) as response:
                            if response.status == 200:
                                content = await response.read()
                                async with aiofiles.open(f"{name}.ts", 'wb') as f:
                                    await f.write(content)
                                    tsName.append(f"{name}.ts")
                                    bar.update()
                                break
                    except:
                        pass
            else:
                async with aiofiles.open(f"{name}.ts", 'wb') as f:
                    await f.write(content)
                    tsName.append(f"{name}.ts")
                    bar.update()



async def main():
    global session
    session=aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20))
    task=[]
    for i in tslist:
        task.append(asyncio.ensure_future(downLoad(i)))
    result=await asyncio.gather(*task)
    await session.close()

def merge_ts_files(ts_files, output_file):
    # 将TS文件列表写入文本文件,因为命令行长度的限制,这里要合并的文件太多
    ts_files.sort()
    with open('ts_files_path.txt', 'w') as file:
        for i in ts_files:
            file.write(f"file '{i}'\n")
    # 使用FFmpeg命令引用文件列表
    cmd = ['ffmpeg', '-f', 'concat', '-safe', '0', '-i', 'ts_files_path.txt', '-c', 'copy', output_file]

    # 调用FFmpeg命令
    subprocess.call(cmd)
def removeTs():
    """
    删除所有ts文件
    :return:
    """
    current_directory=os.getcwd()
    files=os.listdir(current_directory)

    for file in files:
        if file.endswith(".ts"):
            filepath=os.path.join(current_directory,file)
            os.remove(filepath)

if __name__=="__main__":
    getTs()
    bar=tqdm(total=len(tslist),desc="下载进度")
    statTime=time.time()
    asyncio.get_event_loop().run_until_complete(main())
    endTime=time.time()
    print(f"总共用时:{endTime-statTime}")
    merge_ts_files(tsName, "你的名字.mp4")
    removeTs()



九.效果展示

十.一些注意的地方

        1.异步函数或者说协程要用async定义

        2.异步操作的with语句要用async with代替

        3.注意await的使用

        4.会话的开启与关闭都要再一个异步函数里面

  • 27
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值