Python爬取音视频节目

今天做了爬取音视频节目的实验,主要针对两类:一是固有链接的音视频节目,二是非固有链接的音视频节目,现总结相关内容如下:

固有链接的音视频节目

这类网站一般搭建的比较简单,直接获取音视频节目的链接进行保存即可。
例子:https://www.thepaper.cn/list_26913 https://haokan.baidu.com/

非固有链接的音视频节目(以 B 站为例)

这类音视频节目的获取需要考虑三点:

  1. CDN:一个音视频节目是否从多个 CDN 上进行获取;
  2. 反爬策略:构造requests请求的时候需要考虑头部信息,比如 Referer和UserAgent
  3. 音视频整合:B 站爬取得到的是音频和视频内容,分别获取之后需要使用工具进行整合

系统结构

使用到的工具

  1. IntelliJ IDEA 2019.1.4
  2. Python3.8(编程语言)
  3. requests库(发送http请求)
  4. lxml库(xpath解析)
  5. json库(解析json数据)
  6. ffmpeg(合并音频和视频)
    系统数据包库

功能实现原理

  1. 输入 BV 开头音视频节目编号拼接成为有效的包含音视频节目的 url
  2. 构造 request 请求访问视频页面,得到页面的响应信息
  3. 使用 lxml 库与 json 库从返回的响应信息中提取到视频资源的链接
  4. 模拟浏览器请求获取音频和视频资源
  5. 将获得的音频和视频资源合并保存到本地(ffmpeg)

实现代码

了解url结构

到 Bilibili 首页随便点开一个音视频节目,观察节目的 URL,可以看到该 url 包含一个正常的文件路径和一个参数 p 。
-w364

节目的 URL
其中,URL 由 B 站主站域名 + video + 节目编号 + 参数 p 节目集数组成。
有些节目没有选集,则不需要参数 P,构造获取网页信息的时候不添加该参数即可。

解析网页,找到下载视频的链接

打开开发者工具,查找页面元素window.__playinfo__,找到在head标签的第4个 script 标签里面存有视频播放信息,baseUrl 即为视频资源链接
网页结构解析
视频资源 url

下载视频与音频

去查看视频资源的网络请求,发现一共有两种请求方式,一种是 GET,另一种是 OPTION。视频资源可能需要先发送 OPTION 请求,获取服务器许可后再请求资源,许可的保持时间较长,所以只发一次 OPTION 请求就可以了。
GET

OPTION

这里有两种下载视频的方式:
第一种是利用416报错码分片下载,Referer用于写明来源,Range用于规定分片的字节大小以及范围,每下载一定字节的资源,就修改Range,直到最后一次字节数大于剩下的资源时,服务器返回416报错,再重新将Range设置为’Range’: ‘bytes=上一次开始的索引-’。把最后剩下的资源下载下来。每获得一个视频的分片就给他拼接到文件最后,最后得到完整文件。这里使用的是第二种方法,得到了音频和视频两个文件。
第二种是不加入Range参数,直接下载整个的音频与视频,只需要注释掉添加请求头Range参数的语句即可。

合并视频与音频

对于 Bilibili 2018年以后的视频需要再多一步音频与视频合并的操作,这里通过 ffmpeg 来实现这个功能。安装 ffmpeg 参考45。

完整代码

以下代码在参考文献1的基础上进行略微修改,修改部分:combineVideoAudio 函数的命令行输出和 getBiliBiliVideo 函数中 Xpath 定位部分。另外原文是在 Windows 系统下进行的实验,本文在 MAC OS下,因此部分文件路径描述相关问题也有修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import json
import os
import subprocess
import requests
from lxml import etree

# 防止因https证书问题报错
requests.packages.urllib3.disable_warnings()
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36',
'Referer': 'https://www.bilibili.com/'}

'''
获取bilibili视频的主要函数
@param url 视频页面url 结构为:url?参数
@param p 视频p数
@param bv 视频bv数
'''
def getBiliBiliVideo(url, p, bv):
session = requests.session()
res = session.get(url=url, headers=headers, verify=False)
_element = etree.HTML(res.content)
# 获取window.__playinfo__的json对象,[20:]表示截取'window.__playinfo__='后面的json字符串
# videoPlayInfo = str(_element.xpath('//head/script[3]/text()')[0].encode('utf-8').decode('utf-8'))[20:]
videoPlayInfo = str(_element.xpath('//head/script[5]/text()')[0].encode('utf-8').decode('utf-8'))[20:]
videoJson = json.loads(videoPlayInfo)
# 获取视频链接和音频链接
try:
# 2018年以后的b站视频由.audio和.video组成
videoURL = videoJson['data']['dash']['video'][0]['baseUrl']
audioURl = videoJson['data']['dash']['audio'][0]['baseUrl']
flag = 0
except Exception:
# 2018年以前的b站视频音频视频结合在一起,后缀为.flv
videoURL = videoJson['data']['durl'][0]['url']
flag = 1
# 指定文件生成目录,如果不存在则创建目录
dirname = ("./result").encode("utf-8").decode("utf-8")
if not os.path.exists(dirname):
os.makedirs(dirname)
print('文件夹创建成功!')
# 获取每一集的名称
name = bv + "-" + str(p)
# 下载视频和音频
print('正在下载 "' + name + '" 的视频····')
fileDownload(homeurl=url, url=videoURL, name='./result/' + name + '_Video.mp4', session=session)
if flag == 0:
print('正在下载 "' + name + '" 的音频····')
fileDownload(homeurl=url, url=audioURl, name='./result/' + name + '_Audio.mp3', session=session)
# 组合音视频
print('正在组合 "' + name + '" 的视频和音频····')
combineVideoAudio('./result/' + name + '_Video.mp4',
'./result/' + name + '_Audio.mp3',
'./result/' + name + '_output.mp4')
print(' "' + name + '" 下载完成!')


'''
使用session保持会话下载文件
@param homeurl 访问来源
@param url 音频或视频资源的链接
@param name 下载后生成的文件名
@session 用于保持会话
'''
def fileDownload(homeurl, url, name, session=requests.session()):
# 添加请求头键值对,写上 refered:请求来源
headers.update({'Referer': homeurl})
# 发送option请求服务器分配资源
session.options(url=url, headers=headers, verify=False)
# 指定每次下载1M的数据
begin = 0
end = 1024 * 512 - 1
flag = 0
while True:
# 添加请求头键值对,写上 range:请求字节范围
headers.update({'Range': 'bytes=' + str(begin) + '-' + str(end)})
# 获取视频分片
res = session.get(url=url, headers=headers, verify=False)
if res.status_code != 416:
# 响应码不为为416时有数据
begin = end + 1
end = end + 1024 * 512
else:
headers.update({'Range': str(end + 1) + '-'})
res = session.get(url=url, headers=headers, verify=False)
flag = 1
with open(name.encode("utf-8").decode("utf-8"), 'ab') as fp:
fp.write(res.content)
fp.flush()
# data=data+res.content
if flag == 1:
fp.close()
break

'''
用于合并音频与视频
@param videopath 视频路径
@param audiopath 音频路径
@param outpath 生成合并视频的路径
'''
def combineVideoAudio(videopath, audiopath, outpath):
# ffmpeg -i videopath -i audiopath -c:v copy -c:a aac -strict experimental outpath
command1 = 'ffmpeg -i ' + videopath + ' -i ' + audiopath + ' -c:v copy -c:a aac -strict experimental ' + outpath
with open('./result/output.txt', 'w') as file:
subprocess.check_call(command1.encode("utf-8").decode("utf-8"), stderr=file, shell=True)
os.remove(videopath)
os.remove(audiopath)


if __name__ == '__main__':
# 输入bilibili视频的BV号 BV1nE411y76r
bv = input('视频BV号: ')
url = 'https://www.bilibili.com/video/' + bv
# 选择视频从第几p开始到第几p结束
startPart = input('起始P: ')
endPart = input('终止P: ')
for p in range(int(startPart), int(endPart) + 1):
getBiliBiliVideo(url + '?p=' + str(p), p, bv)

例子:https://www.bilibili.com/video/BV1nE411y76r?p=1

参考资料

  1. 如何使用Python爬取bilibili视频(详细教程)https://zhuanlan.zhihu.com/p/148988473
  2. 页面元素定位 XPath 简介 https://www.cnblogs.com/feeland/p/4829093.html
  3. Python - 调用终端执行命令 https://www.aiuai.cn/aifarm949.html
  4. FFmpeg的安装和测试 https://www.jianshu.com/p/0b1c98a28fd4
  5. 用ffmpeg合并音频和视频 https://www.zhihu.com/question/300182407

以下是不太重要的资料:

  1. 各种网站视频下载方法 https://blog.csdn.net/liujiayu2/article/details/86137139
  2. 下载有固定链接的视频 https://blog.csdn.net/liujiayu2/article/details/86016531
  3. 批量抓取网页上的视频 https://blog.csdn.net/u012162613/article/details/41611889

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!