├── example ├── 1.png ├── 2.png └── 3.png ├── run file ├── local.json └── 下载 21-09-07.log ├── README.md ├── local_dict.py └── bilibili.py /example/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeekate/BilibiliFavorateDownloader/HEAD/example/1.png -------------------------------------------------------------------------------- /example/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeekate/BilibiliFavorateDownloader/HEAD/example/2.png -------------------------------------------------------------------------------- /example/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeekate/BilibiliFavorateDownloader/HEAD/example/3.png -------------------------------------------------------------------------------- /run file/local.json: -------------------------------------------------------------------------------- 1 | { 2 | "info":{ 3 | "cover":"http://i1.hdslb.com/bfs/archive/fcd18b4233ad7bbb930e406027db5c64388e40db.jpg", 4 | "intro":"", 5 | "title":"下载" 6 | }, 7 | "local":0, 8 | "media_count":34, 9 | "medias":[], 10 | "status":"new", 11 | "total":0 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # (BFD)Bilibili Favorite Downloader v0.0.1 2 | # 哔哩哔哩 B站 收藏夹视频下载脚本 3 | # 本项目仅供个人交流学习使用,禁止用于商业用途。使用此脚本造成的一切纠纷本人概不负责 4 | ## 1.简介 5 | ### 功能&特性 6 | #### -1 按收藏夹名整理视频,分p视频保存在标题同名文件夹,视频标题为对应p视频标题 7 | #### -2 多线程下载 8 | #### -3 增量下载,收藏夹视频删减仍保留本地视频 9 | #### -4 xml格式弹幕下载,flv/MP4格式可选下载(MP4格式需要mmpeg4工具混流) 10 | #### -5 本地保留收藏夹信息原始数据 11 | ### 1.1环境 12 | #### -1 python3 13 | #### -2 you-get 14 | #### -3 requests 15 | ## 2.使用方法 16 | #### -1 登录B站,打开想要下载到本地的收藏夹。复制浏览器地址栏的收藏夹id(纯数字)![avatar](./example/1.png) 17 | #### -2 打开 bilibili.py ,修改以下三个参数 18 | ``` 19 | 20 | DOWNLOAD_PATH :# #下载路径 21 | 22 | FAV_LIST :# #要下载的收藏夹id 23 | 24 | THREAD_MAX :# #同时下载的视频数 25 | ``` 26 | ![avatar](./example/2.png) 27 | #### -3 双击运行 28 | #### 多个收藏夹下载结果如图![avatar](./example/3.png) 29 | ## 3.注意事项 30 | #### --暂不支持断点续传,请确保脚本运行后网络稳定 31 | #### --若下载时脚本异常退出,或其他原因导致下载中断,请删除收藏夹文件夹下的raw.json 32 | #### --如果you-get提示报错,可能是触发了B站反爬机制,建议更换ip 33 | #### --此脚本暂处于测试阶段,建议不要下载视频过多(>100)的收藏夹 34 | #### --runfile文件夹是脚本运行中产生的文件: 35 | ``` 36 | #### '收藏夹名'+'日期'.log 列出收藏夹视频信息 37 | #### 'Raw.json' api原始数据 38 | #### 'local.json' 已下载视频信息 39 | ``` 40 | ## 4.有希望的 41 | #### --接入[阿里云盘](https://github.com/tickstep/aliyunpan)和[RSS](https://github.com/DIYgod/RSSHub),实现真·自动下载/备份收藏夹视频 42 | #### --加入断点续传 43 | #### --下载信息通过[QQ机器人](https://qmsg.zendee.cn/me.html#/login)或邮箱发送 44 | -------------------------------------------------------------------------------- /local_dict.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | class localJsonType(object): 4 | localNew = {"info":{"cover":"","intro":"","title":""},"medias":[],"local":0,"total":0,"media_count":0,"status":"new"} 5 | 6 | 7 | class saveJson(object): 8 | def save(local, src): 9 | mediaType = {"bv_id":"","bvid":"","cover":"","duration":0,"intro":"","page":0,"pages":[],"pubtime":0,"short_link":"","title":""} 10 | for item in iter(mediaType): 11 | 12 | if (item == 'pages'): 13 | for i in range(src['page']):#分p视频 第(i+1)个视频 14 | pageType = {"dimension":{},"duration":0,"page":0,"title":""} 15 | for pageItem in iter(pageType): 16 | pageType[pageItem] = src['pages'][i][pageItem] 17 | 18 | mediaType['pages'].append(pageType) 19 | else: 20 | mediaType[item] = src[item] 21 | local.append(mediaType) 22 | 23 | #data{} 24 | { 25 | "info": 26 | { 27 | "cover":"收藏夹封面.jpg", 28 | "intro":"收藏夹简介", 29 | "title":"收藏夹标题" 30 | }, 31 | "medias":[], 32 | "local":0, 33 | "total":0, 34 | "media_count":0, 35 | "status":"new" 36 | } 37 | #medias[] 38 | { 39 | "bv_id":"视频bv号", 40 | "bvid":"视频bv号", 41 | "cover":"视频封面.jpg", 42 | "duration":0, 43 | "intro":"视频简介", 44 | 45 | "page":0, 46 | "pages":[], 47 | 48 | "pubtime":0, 49 | 50 | "short_link":"https://b0.tv/BV0Kh0W0Yp", 51 | "title":"视频标题" 52 | } 53 | #pages[] 54 | { 55 | "dimension":{"height":0,"rotate":0,"width":0}, 56 | "duration":0, 57 | "page":0, 58 | "title":"分p标题" 59 | } -------------------------------------------------------------------------------- /run file/下载 21-09-07.log: -------------------------------------------------------------------------------- 1 | 总之就是非常羡慕 2 | 3 | 【收藏级】神级日语现场合集,快来享受这视听盛宴吧!已更新至26P 26p 4 | ├--- 1 《Only My Railgun》 南条爱乃 5 | ├--- 2 giligili爱~超时空要塞Δ 6 | ├--- 3 極乐净土《GARNiDELiA》 7 | ├--- 4 数码宝贝 Butter-Fly 和田光司 8 | ├--- 5 未闻花名~你给我的所有~茅野爱衣 9 | ├--- 6 《直到世界尽头》上杉升&织田哲郎 10 | ├--- 7 ADAMAS《刀剑神域 》LiSA 11 | ├--- 8 恋爱循环~花泽香菜 12 | ├--- 9 团子大家族--茶太 13 | ├--- 10 前前前世《你的名字》 14 | ├--- 11 俺妹op irony【ClariS】 15 | ├--- 12 Don't say lazy 轻音少女 16 | ├--- 13 《天马座幻想》圣斗士星矢 17 | ├--- 14 守护甜心ed《my boy》Buono! 18 | ├--- 15 战姬绝唱~Synchrogazer~水树奈奈 19 | ├--- 16 《打上花火》米津玄师 20 | ├--- 17 核爆神曲《aliez》 21 | ├--- 18 拔剑神曲《βios》 22 | ├--- 19 剑鞘神曲《last stardust》 23 | ├--- 20 断剑神曲《Perfect Time》 24 | ├--- 21 火影忍者《青鸟》 25 | ├--- 22 海贼王《ウィーアー》 26 | ├--- 23 死神《Rolling Star》 27 | ├--- 24 《Barricades》进击的巨人插曲 28 | ├--- 25 《ət'æk 0N tάɪtn》小林未郁 29 | └--- 26 红莲的弓矢-进击的巨人 30 | 31 | 我发现原来我不是喜欢女人 32 | 33 | 每天一遍,青春无极限! 34 | 35 | 成年狗的无奈叹息 36 | 37 | 金 刚 大 占 哥 斯 拉 38 | 39 | 【怀旧经典】 沙宝亮《暗香》火遍大江南北的神曲!《金粉世家》主题曲! 40 | 41 | 猫:不可置信她竟然在反抗朕 42 | 43 | 卧槽!我也想玩 44 | 45 | Tik Tok上一个很火的奶奶....剪辑的手法很有趣,快乐年轻的心态也很棒呀 46 | 47 | 生如蝼蚁,当有干饭之心,命薄如纸,当有干饭之志 48 | 49 | 共轴无刷直驱无人机--A Fully Actuated Aerial Vehicle using Two Actuators 50 | 51 | 微信8.0 K-ON全员 轻音少女 52 | 53 | 【京都动画】京阿尼高燃音乐现场合集 20p 54 | ├--- 1 轻音少女 55 | ├--- 2 吹响!上低音号 56 | ├--- 3 紫罗兰永恒花园 57 | ├--- 4 中二病也要谈恋爱 58 | ├--- 5 小林家的妹抖龙 59 | ├--- 6 凉宫春日的忧郁 60 | ├--- 7 境界的彼方 61 | ├--- 8 日常 62 | ├--- 9 Clannad 63 | ├--- 10 AIR 64 | ├--- 11 Kanon 65 | ├--- 12 无彩限的怪灵世界 66 | ├--- 13 甘城光辉游乐园 67 | ├--- 14 幸运星 68 | ├--- 15 冰菓 69 | ├--- 16 声之形 70 | ├--- 17 利兹与青鸟 71 | ├--- 18 Free! 72 | ├--- 19 SP 空巢老喵中野梓 73 | └--- 20 SP 幸运星 部分动画原版ED 74 | 75 | 【激燃现场】超感动! 轻音少女10周年重聚 这才叫应援 K-ON!@2019拼盘演唱会 76 | 77 | 【中日字幕】轻音少女K-ON! Come with Me LIVE - 相遇天使 78 | 79 | 见过音乐家吵架么?来开开眼吧! 80 | 81 | 大圣,你何时回来啊 82 | 83 | 什么是内摆线 84 | 85 | 课 堂 叫 醒 服 务 86 | 87 | Arduino和HC-SR04 超声波传感器模块还可以这样!!超声波悬浮 88 | 89 | 使用线性弹性驱动器的3D单腿跳 90 | 91 | 贱到极致 92 | 93 | 【倒立摆合集】一、二、三阶倒立摆 4p 94 | ├--- 1 Pendulum on a Cart 95 | ├--- 2 Double Pendulum on a Cart 96 | ├--- 3 Triple Pendulum on a Cart 97 | └--- 4 Side-Stepping of the Triple Pendulum on a Cart 98 | 99 | 这是我见过最沙雕的火影cos视频了,哈哈哈哈哈哈哈哈(听说点赞评论的都有对象了呢!!) 100 | 101 | 大司马打台球完整版 102 | 103 | 死 王 来 电 104 | 105 | 麻雀原来这么叫 106 | 107 | 5G出来后的反应 108 | 109 | 道歉专用视频 显得很有诚意 110 | 111 | 《千千阙歌》- 陈慧娴 最美现场版!岁月鎏金,致敬经典! 112 | 113 | 那时候的调音师太省心了,什么都不用干 114 | 115 | 精选40首经典歌曲合集 116 | 117 | 一位外国小姐姐在停车场的悲催遭遇 118 | 119 | -------------------------------------------------------------------------------- /bilibili.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | #哔哩哔哩收藏夹 4 | 5 | import requests 6 | import json 7 | import os 8 | import time 9 | import re 10 | import sys 11 | import io 12 | import urllib.request 13 | import threading 14 | import random 15 | 16 | from local_dict import localJsonType 17 | from local_dict import saveJson 18 | 19 | 20 | 21 | 22 | # #下载路径 23 | DOWNLOAD_PATH = '#########' 24 | # #要下载的收藏夹id 25 | FAV_LIST = '#########' 26 | # #下载线程数 27 | THREAD_MAX = 10 28 | 29 | 30 | 31 | 32 | 33 | 34 | # #更改输出编码 35 | sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf8') 36 | # #当前日期 37 | date = time.strftime('%y-%m-%d',time.localtime(time.time())) 38 | # #B站视频域名 39 | BILIBILI_URL = 'https://www.bilibili.com/video/' 40 | 41 | 42 | #创建路径 43 | def mkdir(PATH): 44 | folder = os.path.exists(PATH) 45 | if not folder: #判断是否存在文件夹如果不存在则创建为文件夹 46 | os.makedirs(PATH) #makedirs 创建文件时如果路径不存在会创建这个路径 47 | 48 | def loadFavorite(path, favlist): 49 | ##预读取收藏夹信息 50 | #收藏夹api 51 | url = 'https://api.bilibili.com/medialist/gateway/base/spaceDetail?media_id=' + favlist + '&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0&jsonp=jsonp' 52 | #get请求 53 | html = requests.get(url) 54 | #解析json 55 | res = {} 56 | res = json.loads(html.text) 57 | #总内容 58 | media_count = res['data']['info']['media_count'] 59 | if (media_count==0): 60 | print('收藏夹里面没有视频哦,请检查收藏夹id') 61 | exit() 62 | #收藏夹主人昵称 63 | owner = res['data']['info']['upper']['name'] 64 | #收藏夹名 65 | favname = res['data']['info']['title'] 66 | 67 | path = path + '\\' + owner + '\\' + favname 68 | mkdir(path) 69 | 70 | print('下载: ' + owner + ' 的收藏夹 \'' + favname + '\'') 71 | print('路径: ' + path) 72 | print('正在获取收藏夹媒体信息...') 73 | sys.stdout.flush() 74 | ##分页分批次读收藏夹内容 75 | npage=2 76 | while len(res['data']['medias'])|]+', "_", title)#文件名非法检测 158 | mkdir(titlePath) 159 | 160 | ## 下载 161 | #封面下载 162 | coverUrl = media['cover'] 163 | urllib.request.urlretrieve(coverUrl, titlePath + '/' + 'cover.jpg') 164 | 165 | ## 输出视频标题 166 | #记录分p视频 167 | if (page!=1): 168 | flog.writelines(title + ' ' + str(page) + 'p\n') 169 | for i in range(page-1): 170 | flog.writelines(' ├--- ' + str(i+1) + ' ' + media['pages'][i]['title'] + '\n') 171 | flog.writelines(' └--- ' + str(i+2) + ' ' + media['pages'][i+1]['title'] + '\n') 172 | else: 173 | flog.writelines(title + '\n') 174 | flog.writelines('\n') 175 | 176 | #输出分p视频 177 | if (show == 1): 178 | if (page!=1): 179 | print(title + ' ' + str(page) + 'p') 180 | for i in range(page-1): 181 | print(' ├--- ',str(i+1),media['pages'][i]['title']) 182 | print(' └--- ',str(i+2),media['pages'][i+1]['title']) 183 | else: 184 | print(title) 185 | print(' ') 186 | 187 | ##提取you-get下载信息 188 | if (page != 1): 189 | for i in range(page): 190 | downloadName = re.sub(r'[\\/:*?"<>|]+', "_", media['pages'][i]['title'])#文件名非法检测 191 | video_urls.append({ 192 | 'url' : BILIBILI_URL + media['bv_id'] + '?p=' + str(i+1), 193 | 'name' : downloadName, 194 | 'path' : titlePath, 195 | 'size' : media['pages'][i]['dimension'] 196 | }) 197 | else: 198 | downloadName = re.sub(r'[\\/:*?"<>|]+', "_", media['pages'][0]['title'])#文件名非法检测 199 | video_urls.append({ 200 | 'url' : BILIBILI_URL + media['bv_id'], 201 | 'name' : downloadName, 202 | 'path' : titlePath, 203 | 'size' : media['pages'][0]['dimension'] 204 | }) 205 | 206 | flog.close() 207 | sys.stdout.flush() 208 | return video_urls 209 | 210 | 211 | def get_max_resolution(res): 212 | if (res >= 1080): 213 | return ' ' 214 | elif (res >= 720): 215 | return '720 ' 216 | elif (res >= 480): 217 | return '480 ' 218 | elif (res >= 360): 219 | return '360 ' 220 | 221 | ##多线程参考: 222 | ##https://www.cnblogs.com/hanmk/p/12990017.html 223 | 224 | def main(url, video_name, save_path, size): 225 | resolution = min(int(size['height']), int(size['width'])) 226 | resolute = get_max_resolution(resolution) 227 | 228 | """ 229 | 主函数:实现下载图片功能 230 | :param url: 图片url 231 | :param image_name: 图片名称 232 | :return: 233 | """ 234 | with pool_sema: 235 | print('正在下载: {}'.format(video_name)) 236 | sys.stdout.flush() 237 | #完整文件路径(包含文件名及后缀) 238 | file_path = save_path + '\\' + video_name + '.flv' 239 | you_get_vedio = 'you-get --format=flv' + resolute + '-o "' + save_path + '" -O "' + video_name + '" ' + url 240 | if not os.path.exists(file_path): # 判断是否存在文件,不存在则爬取 241 | try: 242 | # print('\t' + you_get_vedio) 243 | os.system(you_get_vedio) 244 | #time.sleep(random.randint(4,9)) 245 | # print('\t下载成功:{}'.format(video_name)) 246 | except: 247 | print(video_name, " 下载失败") 248 | 249 | else: 250 | print(video_name, '已存在') 251 | sys.stdout.flush() 252 | class MyThread(threading.Thread): 253 | """继承Thread类重写run方法创建新进程""" 254 | def __init__(self, func, args): 255 | """ 256 | 257 | :param func: run方法中要调用的函数名 258 | :param args: func函数所需的参数 259 | """ 260 | threading.Thread.__init__(self) 261 | self.func = func 262 | self.args = args 263 | 264 | def run(self): 265 | self.func(self.args[0], self.args[1], self.args[2], self.args[3]) 266 | # 调用func函数 267 | # 因为这里的func函数其实是上述的main()函数,它需要3个参数;args传入的是个参数元组,拆解开来传入 268 | 269 | if __name__ == '__main__': 270 | 271 | remote_list = {} 272 | remote_list = loadFavorite(DOWNLOAD_PATH, FAV_LIST) 273 | 274 | local_list = {} 275 | local_list = loadLocalInfo(DOWNLOAD_PATH, remote_list) 276 | 277 | down_list = [] 278 | down_list = showList(DOWNLOAD_PATH, remote_list, 0) 279 | 280 | pool_sema = threading.BoundedSemaphore(THREAD_MAX) # 或使用Semaphore方法 281 | 282 | print('正在下载收藏夹\'{}\'\n\n'.format(remote_list['data']['info']['title'])) 283 | sys.stdout.flush() 284 | 285 | thread_list = [] 286 | for t in down_list: 287 | m = MyThread(main, (t["url"], t["name"], t["path"], t["size"])) # 调用MyThread类,得到一个实例 288 | thread_list.append(m) 289 | 290 | for m in thread_list: 291 | m.start() # 调用start()方法,开始执行 292 | 293 | for m in thread_list: 294 | m.join() # 子线程调用join()方法,使主线程等待子线程运行完毕之后才退出 295 | 296 | ######下载之前判定文件是否存在::根据文件名(若up主改分p标题,也会重新下载:增量下载) 297 | ######多线程(线程数可选)下载 298 | ######annie 作者:思思陆思思 https://www.bilibili.com/read/cv5720475/ 出处:bilibili 299 | ######you-get反反爬 300 | ######you-get 多文件/多p下载,分p命名 301 | ######断点续传 302 | --------------------------------------------------------------------------------