大家好,我是小小明。

今天我将带大家一步步来研究如何下载B站直播视频。

获取直播间下载地址

首先获取指定分区直播间id列表:

import requests
from lxml import etree


def get_room_ids(room_type="学习"):
    urls = {"学习": "https://live.bilibili.com/p/eden/area-tags?visit_id=9ynmsmaiie80&areaId=377&parentAreaId=11",
            "颜值领域": "https://live.bilibili.com/show/yzly?visit_id=3g19a7bxnb60"}
    res = requests.get(urls[room_type])
    res.encoding = res.apparent_encoding
    html = etree.HTML(res.text)
    room_ids = {}
    for a in html.xpath("//ul/li/a"):
        url = a.xpath("./@href")[0]
        tags = a.xpath(".//text()")
        room_ids[tags[1]] = url[1:url.find("?")]
    return room_ids


room_ids = get_room_ids()
room_ids
{'公考课堂——刘文超': '21283497',
 '工业算法选型及调优策略': '21689802',
 '是个废狮': '22029374',
 '进行一个模型的做': '1070197',
 'Lumist Navi——简历面试指南': '22518797',
 '教建模的大叔': '22590752',
 '随便剪剪': '36431',
 'blender,干空间站': '4874724',
 '【若鹭姬工作室】游戏制作日常': '5688995',
 '【建模】nomad 建模': '1261365',
 '戳戳戳': '969007',
 '全能音乐人入门课第二期-第一节直播': '93751',
 'PS学习分享间': '14352877',
 '马士兵老师两天带你彻底征服多线程!': '21365314',
 '十道真题搞定SQL': '11209769',
 '超nice的扁平风IP形象插画来了!': '1327479',
 '直播作曲&编曲~': '71040',
 '<python副业>月入2万+直播分享': '23202081',
 '前端编程小姐姐在线写游戏网页': '22273117',
 '零基础学Python/前端': '14584642'}

获取了当前时间学习分区中最热门的前N个直播间id,下面我计划获取PS学习分享间的直播间。

获取指定直播间m3u8视频流地址:

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"
}

def get_live_url(cid, platform='h5'):
    playUrl = 'https://api.live.bilibili.com/xlive/web-room/v1/playUrl/playUrl'
    params = {
        'cid': cid,  # cid序列号
        'qn': 150,  # 播放的视频质量
        'platform': platform,  # 视频的播放形式
        'ptype': 16
    }
    response = requests.get(playUrl, headers=headers, params=params).json()
    text = response['data']['durl']
    url = text[-1]['url']
    return url


url = get_live_url(room_ids['PS学习分享间'])
url
'https://d1--cn-gotcha103.bilivideo.com/live-bvc/779295/live_346651764_2556686_1500.m3u8?cdn=cn-gotcha03&expires=1624891101&len=0&oi=1947748628&pt=h5&qn=150&trid=100375c02ceb030e4ee082e77a292b8bba6e&sigparams=cdn,expires,len,oi,pt,qn,trid&sign=ecd48c9e8c8ea467004377dedd16a384&ptype=0&src=5&sl=1&sk=677197cc63610fc&order=4'

使用m3u8模块解析一下该地址:

import m3u8

playlist = m3u8.load(uri=url, headers=headers)
print("segments:", playlist.segments)
print("target_duration:", playlist.target_duration)
print("keys:", playlist.keys)
print("playlists", playlist.playlists)
segments: 
target_duration: None
keys: []
playlists #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=10000000
https://236809367.cloudvdn.com/a.m3u8?cdn=cn-gotcha03&domain=d1--cn-gotcha103.bilivideo.com&expires=1624891101&len=0&oi=1947748628&order=4&player=bTMAAGE2uvzMwowW&pt=h5&ptype=0&qn=150&secondToken=secondToken%3AAU83VLPVHkaN2K6QFa3M4hu5i9I&sign=ecd48c9e8c8ea467004377dedd16a384&sigparams=cdn%2Cexpires%2Clen%2Coi%2Cpt%2Cqn%2Ctrid&sk=677197cc63610fc&sl=1&src=5&streamid=live-qn%3Alive-qn%2Flive_346651764_2556686_1500&trid=100375c02ceb030e4ee082e77a292b8bba6e&v3=1

只有playlists有数据,说明该m3u8是个嵌套类型的。

接下来解析出内部的m3u8地址:

def get_real_url(url):
    playlist = m3u8.load(uri=url, headers=headers)
    return playlist.playlists[0].absolute_uri

real_url = get_real_url(url)
real_url
'https://236809362.cloudvdn.com/a.m3u8?cdn=cn-gotcha03&domain=d1--cn-gotcha103.bilivideo.com&expires=1624891101&len=0&oi=1947748628&order=4&player=BEEAAPDKUQjywowW&pt=h5&ptype=0&qn=150&secondToken=secondToken%3A2KUkkU8RBKYtssdUqEbh-z-vpkQ&sign=ecd48c9e8c8ea467004377dedd16a384&sigparams=cdn%2Cexpires%2Clen%2Coi%2Cpt%2Cqn%2Ctrid&sk=677197cc63610fc&sl=1&src=5&streamid=live-qn%3Alive-qn%2Flive_346651764_2556686_1500&trid=100375c02ceb030e4ee082e77a292b8bba6e&v3=1'

注意:内部m3u8地址,会在指定时间内仍然未连接时自动失效,持续被访问的内部地址则持续有效。失效时,需要重新从外部地址获取(再次调用上述方法即可)。

解析一下内部m3u8地址:

playlist = m3u8.load(uri=real_url, headers=headers)
print("segments:", playlist.segments)
print("target_duration:", playlist.target_duration)
print("keys:", playlist.keys)
print("playlists", playlist.playlists)
segments: #EXTINF:1.034,
/1623780718.ts?domain=d1--cn-gotcha103.bilivideo.com&keyoff=0&player=BEEAAG7TT8s8w4wW&streamid=live-qn%3Alive-qn%2Flive_346651764_2556686_1500&v3=1
#EXTINF:2.485,
/1623780719.ts?domain=d1--cn-gotcha103.bilivideo.com&player=BEEAAG7TT8s8w4wW&streamid=live-qn%3Alive-qn%2Flive_346651764_2556686_1500&v3=1
target_duration: 3.0
keys: [None]
playlists 

可以看到,通过segments属性,可以获取ts视频流地址。keys属性为None,说明视频流未加密,这样也保证了我们的下载会非常简单。

可以通过以下代码获取ts文件的时间戳和下载地址:

for seg in playlist.segments:
    print(int(seg.uri[1:seg.uri.find(".")]))
    print(seg.absolute_uri)

m3u8介绍

M3U8是Unicode 版本的 M3U,用 UTF-8 编码。"M3U"和"M3U8"文件都是苹果公司使用的HTTP Live Streaming 格式的基础,这种格式可以在 iPhone 和 Macbook 等设备播放。是一种播放多媒体列表的文件格式,文本内容是一系列媒体片段资源,顺序播放该片段资源,即可完整展示多媒体资源。其格式大致如下:

# 未加密
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:4.000000,
1af12fece7a000000.ts
#EXTINF:4.320000,
1af12fece7a000001.ts
...
#EXTINF:3.800000,
1af12fece7a001155.ts
#EXT-X-ENDLIST

# 加密
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="https://ts1.yuyuangewh.com:9999/20200808/1XdSSbTb/2000kb/hls/key.key"
#EXTINF:3,
https://ts1.yuyuangewh.com:9999/20200808/1XdSSbTb/2000kb/hls/EUtRrqJU.ts
#EXTINF:4.72,
https://ts1.yuyuangewh.com:9999/20200808/1XdSSbTb/2000kb/hls/HF90vrrN.ts
...
#EXTINF:0.24,
https://ts1.yuyuangewh.com:9999/20200808/1XdSSbTb/2000kb/hls/b7ZLcRqT.ts
#EXT-X-ENDLIST

m3u8文件中常见的标签:

标签格式作用
EXTM3U#EXTM3U表明该文件是一个m3u8文件,每个m3u8文件必须将该标签放置在第一行
EXT-X-VERSIONEXT-X-VERSION:<number>表明该文件是一个m3u8文件,每个m3u8文件必须将该标签放置在第一行
EXT-X-TARGETDURATION#EXT-X-TARGETDURATION:<s>表示每个视频分段最大的时长(单位秒)
EXT-X-PLAYLIST-TYPE#EXT-X-PLAYLIST-TYPE:<type-enum>表明流媒体类型,VOD 表示该视屏流为点播源,因此服务器不能更改该m3u8文件;EVENT表示该视频流为直播源,因此服务器不能更改或删除该文件任意部分内容,但是可以在文件末尾添加新内容
EXT-X-MEDIA-SEQUENCE#EXT-X-MEDIA-SEQUENCE:<number>表示播放列表第一个URL片段文件的序列号,每个媒体片段URL都拥有一个唯一的整型序列号,每个媒体片段序列号按出现顺序依次加 1,如果该标签未指定,则默认序列号从0开始
EXT-X-KEY#EXT-X-KEY:METHOD=AES-128,URI="http:xxxx",IV="xxxx"表明视频流文件的加解密方法,METHOD表示加密方式,URI表示密钥路径,该密钥是一个 16 字节的数据,IV是一个128位的十六进制数值
EXTINF#EXTINF:<duration>,[<title>]表示其后 URL 指定的媒体片段时长(单位为秒),duration可以为十进制的整型或者浮点型,其值必须小于或等于EXT-X-TARGETDURATION指定的值
EXT-X-ENDLIST#EXT-X-ENDLIST表明m3u8文件的结束

更多可参考:https://www.jianshu.com/p/e97f6555a070

一个专门用于解析m3u8文件库,参阅:https://pypi.org/project/m3u8/

pip install m3u8

下载直播

首先测试访问10次链接并下载:

import time


def download_video(max_count=1000, max_size=120*1024*1024):
    max_id = None
    size = 0
    for i in range(1, max_count+1):
        playlist = m3u8.load(uri=real_url, headers=headers)
        for seg in playlist.segments:
            current_id = int(seg.uri[1:seg.uri.find(".")])
            if max_id and current_id <= max_id:
                continue
            with open("combine.mp4", "ab" if max_id else "wb") as f:
                r = requests.get(seg.absolute_uri, headers=headers)
                data = r.content
                size += len(data)
                f.write(data)
                print(f"\r下载次数({i}/{max_count}),已下载:{size/1024/1024:.2f}MB", end="")
                if size >= max_size:
                    print("\n文件已经超过大小限制,下载结束!")
                    return
        max_id = current_id
        time.sleep(2)


download_video(10)
下载次数(10/10),已下载:3.88MB                                                                          | 0/1 [00:21<?, ?it/s]s]

经测试下载成功。两个参数分别限制了最大的访问次数,和最大的文件大小。任何一个参数满足条件都会结束下载,当前直播间停播时,程序也会抛出异常自动停止。

完整代码:

import time
import m3u8
import requests

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"
}


def get_live_url(cid, platform='h5'):
    playUrl = 'https://api.live.bilibili.com/xlive/web-room/v1/playUrl/playUrl'
    params = {
        'cid': cid,  # cid序列号
        'qn': 150,  # 播放的视频质量
        'platform': platform,  # 视频的播放形式
        'ptype': 16
    }
    response = requests.get(playUrl, headers=headers, params=params).json()
    text = response['data']['durl']
    url = text[-1]['url']
    return url


def get_real_url(url):
    playlist = m3u8.load(uri=url, headers=headers)
    return playlist.playlists[0].absolute_uri


def download_video(url, max_count=1000, max_size=120*1024*1024):
    max_id = None
    size = 0
    for i in range(1, max_count+1):
        playlist = m3u8.load(uri=url, headers=headers)
        for seg in playlist.segments:
            current_id = int(seg.uri[1:seg.uri.find(".")])
            if max_id and current_id <= max_id:
                continue
            with open("combine.mp4", "ab" if max_id else "wb") as f:
                r = requests.get(seg.absolute_uri, headers=headers)
                data = r.content
                size += len(data)
                f.write(data)
                print(
                    f"\r下载次数({i}/{max_count}),已下载:{size/1024/1024:.2f}MB", end="")
                if size >= max_size:
                    print("\n文件已经超过大小限制,下载结束!")
                    return
        max_id = current_id
        time.sleep(2)


url = get_live_url('22273117')
real_url = get_real_url(url)
download_video(real_url)

image-20210628224520081

image-20210628224139434

下载了一个公考的直播,发现就是文件实在是太大了,14分钟的视频居然就占到了100MB。


本文转载:CSDN博客