大家好,我是小小明。
今天我将带大家一步步来研究如何下载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-VERSION | EXT-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)
下载了一个公考的直播,发现就是文件实在是太大了,14分钟的视频居然就占到了100MB。