自己练习网上抓东西。
实现功能:检索、下载b站非会员视频,会员的得要会员cookie。
核心部分:对b站公共api的使用,也让我对什么是api有了一定的了解。
程序设计思路:获得bvid→获得cid→获得视频url→下载flv
感想:不知道为什么,感觉这都是一些表面的东西,没什么实质的技术含量,也许是自己现在还比较菜,也算是基础入门吧。
难点: 正则表达式的使用,这也是我觉得比较难的地方吧。我也想过用beautifulsoup4,尝试了一下放弃了,因为bs4还是要用到正则(主要是我还没学透),
之后我会更多的投入到大学基础课程的学习。啊!俺的高数,啊!俺的English,俺来啦~
正文:写这篇文章主要是想总结一下,一些难点,心得。
感谢:Bilibili ORZ
b站对视频资源设计是有了bvid(avid)视频ID和cvid视频分段ID(就是“自古2p没人看的‘p’ ”)就能从b站api获取视频url,用这个url就能从b站服务器下载视频。
从b站的search服务器网站’https://search.bilibili.com/all?keyword=’
'keyword='在后面加上想要搜索的名称
就能得到索引html文档。用正则表达式提取文档中bvid、视频名字和该视频的url(以后headers头中referer要用,防止反爬)
pattern = compile(')# 获取Bvid、视频名字
data = re.findall(patten, html)# 正则匹配
list = []# 用来保存由 序号、名字、bvid、referer构成的字典1
i = 0
for ul, name in data:
dict = {'number': '', 'name': '', 'bvid': '', 'referer': ''}
i += 1
referer = 'http://www.bilibili.com' + ul
bvid = re.findall('/([a-zA-Z0-9]*)\?', ul)[0]
dict['number'] = i
dict['name'] = name
dict['bvid'] = bvid
dict['referer'] = referer
list.append(dict)
path = savefile.save(name=s_name, dirpath=root_dir, content=list, suffix='json')
# 我为了更好的测试,将每一步的结果都保存了起来,这个savefile函数是我自己写的。
从b站的api"https://api.bilibili.com/x/player/pagelist?bvid=BV1C7411k7D3&jsonp=jsonp"获得cid
'bvid='后面加上获得的bvid就可以了。
def getcid(name, bvid, referer, dirpath):
Host = 'api.bilibili.com'
url = 'https://api.bilibili.com/x/player/pagelist?'+'bvid=' + bvid +'&jsonp=jsonp'
path = getHTML.get(url=url, name=name, dirpath=dirpath, referer=referer, Host=Host, suffix='txt')
return path
getHTML是我自己写的用于爬取的函数,因为要经过不止一次爬取
内容是:(这才是真正意义上的爬取)
from urllib import request
import savefile
def get(url, name, dirpath=None, referer=None, Host=None, suffix=None):
headers = {
'Accept': 'text/html, application/xhtml+xml, application/xml; q=0.9, */*; q=0.8',
'Accept-Language': 'zh-Hans-CN, zh-Hans; q=0.5',
'DNT': '1',
'Host': 'search.bilibili.com',
'Origin': 'https://www.bilibili.com',
'Upgrade-Insecure-Requests': '1',
'User-Agent': '写上自己的useragent,一个网页右键检查元素,network网络里随便点一个包request headers请求头里',
}# 刚开始尝试的时候headers写的少,被b站大大封了几次ip,还好b站大大仁慈,cookie可以不加,如果是浏览器无痕模式一般是不会将cookie加入头的,加上'DNT':1
if referer != None:
headers['referer'] = referer
if Host != None:
headers['Host'] = Host
req = request.Request(url, headers=headers)
print('开始抓取')
response = request.urlopen(req)
print('抓取{}成功!'.format(url))
html = response.read()
html = html.decode('utf-8')
path = savefile.save(name=name, dirpath=dirpath, content=html,suffix=suffix )
return path
if __name__ == '__main__':
获取的cid数据是json格式的
用json模块读取,可以进行一系列字典、列表操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m4huHqqu-1588317947898)(cid.png)]
如果直接用瞅json文本结构简单还好,结构复杂能瞅瞎眼。我用vscode里的json tool插件,安装并重启后按ctrl + alt + m即可进行结构化展示,注意格式化前不要选中(就是浏览时用光标一直按住左键选取,文字地下会出现阴影)任何文本,不然会失败。
import json
with open(cid_file, mode='r', encoding='utf-8') as f:
f = json.load(f)
如
f['data'][0]['cid']
值就是145508985。
注意data这个键对应的值是个列表,列表里的元素又是很多个(这里只有一个)字典。
有一些视频里不止有一个p,一个p对应一个cid。
现在已经取得了视频的bvid,cid就能利用这两个参数从b站api
'https://api.bilibili.com/x/player/playurl?' + 'bvid=' + bvid +'&cid=' + str(cid) + '&qn=64&type=&otype=json'
里获得url,qu=64是视频分辨率1080p,32是720p。
这里获取资源的时候headers里Host值换成 api.bilibili.com
referer值换成之前已经获取的referer,一般结构是:“http://www.bilibili.com/video/BV1C7411k7D3?from=search”,
爬取后的文件格式依旧是json
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nXkAPk6g-1588317947901)(2.png)]
要有耐心滴一层一层剥开(我愿一层一层一层一层剥开你的心~)
import json
with open(cid_file, mode='r', encoding='utf-8') as f:
f = json.load(f)
f['data']['durl'][0]['url']# 这个就是主要的那个url,backup是备份的意思。
durl的值是个列表,如果是个多p视频,列表里的元素就会有好多个。
import requests
def download_video(url, referer, name, p_dir):
headers = {
'origin': 'https://www.bilibili.com',
'referer': '还是之前那个referer',
'user-agent': '还是那个useragent'
}
headers['referer'] = referer
r = requests.get(url, stream=True, headers=headers) # 这里是下载
length = float(r.headers['content-length'])
f = open(p_dir + '\\' + '{}.{}'.format(name, 'flv'), 'wb')
count = 0
count_tmp = 0
time1 = time.time()
print(url)
print('开始下载{}'.format(url))
for chunk in r.iter_content(chunk_size=10240):# 这里开始是进行分块保存,并计时
if chunk:
f.write(chunk)
count += len(chunk)
if time.time() - time1 > 2:
p = count / length * 100
speed = (count - count_tmp) / 1024 / 1024 / 2
count_tmp = count
print(name + ': ' + '{:.2f}'.format(p) + '%' + ' Speed: ' + '{:.2f}'.format(speed) + 'M/S')
time1 = time.time()
else:
print('{}保存失败!'.format(name))
f.close()
print("{0}.{1}保存成功!".format(name, 'flv'))
以上部分就能实现从搜索到下载视频了
有些命名时给我自己看的,看不懂没关系,1.4以上已经都实现了。
from urllib import parse
import getHTML
import creatdir
import getPATH
import getBvidUrlName
import printSelectList
import getCid
import getPlayurl
import download
import title
title.content()
d_path = getPATH.GetDesktopPath()
name = input('请输入你想下载的视频名称: ')
print('桌面路径为:{}'.format(d_path))
path = input('输入你想保存的文件夹的绝对位置(输入p跳过此步骤,默认路径为桌面):')
# 编码
parse_name = parse.quote(name)
search_url = 'https://search.bilibili.com/all?keyword=' + parse_name
# 创建根文件夹
if path != 'p':
root_dir = creatdir.creat_a_dir(name, path)
else:
root_dir = creatdir.creat_a_dir(name, d_path)
# 获取HTML
html_file = getHTML.get(url=search_url, name=name, dirpath=root_dir, suffix='txt')
# 抽取符合条件的列单
video_list = getBvidUrlName.getlist(html_file, name, root_dir=root_dir)
# 打印列单,让用户选择,获得选择视频的bvid,名字,和网址
g_name, bvid, referer = printSelectList.select_list(video_list)
# 创建用户选择视频的文件夹
g_dir = creatdir.creat_a_dir(g_name, root_dir)
# 获得cid
cid_file = getCid.getcid(g_name, bvid, referer, g_dir)
# 获取被选中的所有视频
p_name_list, p_dir_list = getPlayurl.getURL(cid_file=cid_file, bvid=bvid, referer=referer, g_dir=g_dir)
# 下载视频
download.get_video(g_dir=g_dir, p_name_list=p_name_list, p_dir_list=p_dir_list, referer=referer)
import os
import time
def creat_a_dir(sname, spath=r'C:\Users\PAN\Desktop'):
'''创建位于spath的名为sname+当地时间的文件夹。默认在桌面,返回文件夹的目录'''
local_time = time.strftime('%Y-%m-%d-%H-%M-%S',time.localtime())
dir_path = spath + '\\' + sname + '-' + local_time
os.mkdir(dir_path)
print('创建 {} 成功!'.format(dir_path))
return dir_path
if __name__ == '__main__':
a = creat_a_dir('小甲鱼')
print(a)
import json
def save(name, dirpath, content, suffix=None):
'''name 文件名字, dirpath 保存在的文件夹目录,content 保存内容, suffix 后缀'''
if suffix == 'json':
suffix = '.' + suffix
path = dirpath + '\\' + name + suffix
with open(path, mode='a', encoding='utf-8') as f0:
json.dump(content, f0)
else:
suffix = '.' + 'txt'
path = dirpath + '\\' + name + suffix
with open(path, mode='a', encoding='utf-8') as f1:
f1.write(content)
print('保存 {} 成功!'.format(path))
return path
其实json是一种带格式的文本文件。保存成txt格式,用json格式化读取,结果是一样的。
我折腾来折腾去算明白了,只要是文本,统一保存txt就没错,文本的内容格式不用导来导去。关键在于编码要搞清楚。
很清楚,有很多地方可以改进。
如果有时间的话会继续做。
优秀的程序,也要有优秀的异常处理,我这里并没有搞什么异常处理。程序可执行,但是比较脆弱。
欢迎到我的个人博客评论,大一新生,一起进步。(希望有大神能教教我怎么下会员的flv手动滑稽)
https://tominochick.github.io/