由于B站没有PC客户端,电脑下载视频很不方便,遂使用Tk编写一款B站视频下载工具,输入一个网址选择清晰度之后就能够下载对应的视频,可以下载单P、合集、合集单P,使用可视化GUI图形界面,交互性更强,来吧,展示~


一.准备工作

tkinter、os系统模块、re正则模块、subprocess新的进程模块、还有本次比较重要的ffmpeg.exe用于视频和音频的合并,关于ffmpeg请参考:

ffmpeg - 百度百科

二.预览

1.启动

2.解析





解析出多个清晰度视频以供下载

3.下载中

4.下载完成



分别下载完视频和音频后,对它们进行合并,最后输出一个完整的视频文件

5.结果



1080P+,针不戳

三.设计流程

1.bilibili_video_spider

2.视频json的查找

  1. 首先查看网页源代码
  2. 在网页的这个js里,能够找到关于视频的相关视频、音频、视频质量、长度、格式...等信息,直接正则截取就好啦
  3. 紧接着,下面这个js里,就是视频的aid、分P信息、up主信息、相关视频推荐信息,也用正则就能截取

四.源代码

1.Bilibili_Video_Downloader-GUI

  1. from tkinter import *
  2. from tkinter import ttk
  3. from tkinter import messagebox
  4. import os
  5. import threading
  6. from bilibili_video_spider import Bibili_Video_Spider as sp2
  7. import re
  8. from my_util import My_Util
  9. """
  10. GUI+Spider
  11. """
  12. class App:
  13. def __init__(self):
  14. self.base_dir = './bilibili_videos/'
  15. self.start_flag=''
  16. self.has_more_flag=''
  17. self.spider=sp2()
  18. self.create_widget()
  19. self.set_widget()
  20. self.place_widget()
  21. self.window.mainloop()
  22. def create_widget(self):
  23. self.window = Tk()
  24. self.window.title('Bilibili_Video_Downloader-v1.0')
  25. width = 450
  26. height = 520
  27. screen_width = self.window.winfo_screenwidth()
  28. screen_height = self.window.winfo_screenheight()
  29. left = (screen_width - width) / 2
  30. top = (screen_height - height) / 2
  31. self.window.geometry("%dx%d+%d+%d" % (width, height, left, top))
  32. self.window.resizable(0, 0)
  33. self.l1 = ttk.Label(self.window, text='请输入视频链接地址:')
  34. self.e1_var=StringVar()
  35. self.e1 = ttk.Entry(self.window, width=90,textvariable=self.e1_var)
  36. self.l5 = ttk.Label(self.window, text='选择清晰度:')
  37. self.combobox=ttk.Combobox(self.window,state='readonly',width=15,justify='center')
  38. self.l2 = ttk.Label(self.window, text='当前状态:')
  39. self.t1 = Text(self.window, width=80, height=20)
  40. self.l3_var=StringVar()
  41. self.l3 = ttk.Label(self.window, text='当前下载进度:',textvariable=self.l3_var)
  42. self.progress=ttk.Progressbar(self.window,orient=HORIZONTAL,length=400,mode='determinate',value=0,maximum=100)
  43. self.l4_var = StringVar()
  44. self.l4_var.set('0.0%[未下载]')
  45. self.l4 = ttk.Label(self.window, textvariable=self.l4_var)
  46. self.b1 = ttk.Button(self.window, text='解析', command=lambda: self.thread_it(self.pre_analysis))
  47. self.b2 = ttk.Button(self.window, text='下载', command=lambda: self.thread_it(self.donwload_video))
  48. def set_widget(self):
  49. self.window.protocol('WM_DELETE_WINDOW', self.quit_window)
  50. self.window.bind('<Escape>', self.escape)
  51. self.e1.bind('<Return>', self.enter)
  52. self.b2.config(state=DISABLED)
  53. self.combobox.config(value=['--请先解析--'])
  54. self.combobox.current(0)
  55. def place_widget(self):
  56. self.l1.pack(anchor="w")
  57. self.e1.pack(anchor="w", padx=20)
  58. self.l5.pack(anchor="w",pady=5)
  59. self.combobox.pack(anchor="center")
  60. self.l2.pack(anchor="w")
  61. self.t1.pack(anchor="w", padx=20)
  62. self.l3.pack(anchor="w",pady=5)
  63. self.progress.pack(pady=5)
  64. self.l4.pack()
  65. self.b1.pack(side='left', padx=90)
  66. self.b2.pack(side='left', padx=10)
  67. def pre_analysis(self):
  68. input_video_link = self.e1.get()
  69. input_video_link=input_video_link.strip()
  70. if input_video_link.startswith(r'https://www.bilibili.com/video/'):
  71. if '&' in input_video_link:
  72. raw_link=input_video_link.split('&')[0]
  73. else:
  74. raw_link=input_video_link
  75. try:
  76. #av 转 bv
  77. av_number = int(re.findall('https://www.bilibili.com/video/av(\d+)?', raw_link)[0])
  78. url=raw_link.replace(av_number,My_Util().av_convert_bv(av_number))
  79. except IndexError:
  80. url=raw_link
  81. self.spider.set_start_url(url)
  82. self.spider.get_page_html()
  83. self.video_number = self.spider.get_video_number()
  84. base_title = self.spider.get_video_title()
  85. if re.match('https://www.bilibili.com/video/.*\?p=\d+',url):
  86. current_num=re.findall('https://www.bilibili.com/video/.*\?p=(\d+)',url)
  87. self.has_more_flag=True
  88. self.current_video_title=self.spider.part_name_list[int(current_num[0])]
  89. else:
  90. self.has_more_flag=False
  91. self.current_video_title=base_title
  92. self.entrace_url=url
  93. self.analysis_videos(url)
  94. if self.start_flag!=True:
  95. self.b2.config(state=NORMAL)
  96. # self.b1.config(state=DISABLED)
  97. else:
  98. messagebox.showwarning('警告', '请输入正确的分享链接!')
  99. self.e1_var.set('')
  100. def analysis_videos(self,url):
  101. """
  102. :param url:
  103. :return:
  104. """
  105. My_Util().do_makedirs(self.base_dir)
  106. self.video_item_ = self.spider.get_video_and_audio(self.spider.get_video_detail_json())
  107. video_quality_list=[]
  108. for video_detail in self.video_item_['video_detail']:
  109. for data in video_detail.items():
  110. video_quality_list.append(data[0])
  111. self.combobox.config(value=video_quality_list)
  112. self.combobox.current(0)
  113. self.t1.delete(0.0,END)
  114. self.insert_to_t1(f'[视频标题]:{self.current_video_title}')
  115. self.insert_to_t1(f'[视频时长]:{self.video_item_["video_length"]}')
  116. self.insert_to_t1(f'[视频清晰度]:{" ".join(video_quality_list)}')
  117. self.insert_to_t1(f'请选择清晰度后点击下载按钮---------------',time_str=False)
  118. def donwload_video(self):
  119. self.start_flag=True
  120. self.b2.config(state=DISABLED)
  121. if self.has_more_flag:
  122. ret = messagebox.askyesno('提示', '此视频包含多P,是否下载全集?')
  123. if ret:
  124. download_more=True
  125. else:
  126. download_more=False
  127. else:
  128. download_more=False
  129. for i in range(self.video_number):
  130. if download_more:
  131. begin_url = self.entrace_url.split('?')[0] + f'?p={i+1}'
  132. self.spider.video_title = self.spider.part_name_list[i]
  133. current_title=self.spider.part_name_list[i]
  134. else:
  135. begin_url=self.entrace_url
  136. self.spider.video_title = self.current_video_title
  137. current_title =self.current_video_title
  138. self.insert_to_t1(f'开始下载{current_title}---------------')
  139. self.l3_var.set('视频下载进度:')
  140. self.spider.set_start_url(begin_url)
  141. video_item_ = self.spider.get_video_and_audio(self.spider.get_video_detail_json())
  142. video_url_list=[]
  143. for video_detail in video_item_['video_detail']:
  144. for data in video_detail.items():
  145. video_url_list.append(data[1])
  146. download_url = video_url_list[self.combobox.current()]
  147. current_video_name=self.spider.part_name_list[i]
  148. for progrees, speed in self.spider.down_video(download_url,):
  149. self.progress['value'] = progrees
  150. self.l4_var.set(f'进度:%.1f%% 速度:%s' % (progrees, speed))
  151. self.progress.update()
  152. self.insert_to_t1(f'[{current_video_name}视频下载完成...')
  153. self.l4_var.set('100%[下载完成]')
  154. self.insert_to_t1('-' * 30)
  155. audio_url = video_item_['audio_url']
  156. self.insert_to_t1(f'开始下载{current_title}音频---------------')
  157. self.l3_var.set('音频下载进度:')
  158. for progrees, speed in self.spider.downlonad_autio(audio_url,):
  159. self.progress['value'] = progrees
  160. self.l4_var.set(f'进度:%.1f%% 速度:%s' % (progrees, speed))
  161. self.progress.update()
  162. self.insert_to_t1(f'[{current_video_name}音频下载完成...')
  163. self.l4_var.set('100%[下载完成]')
  164. self.insert_to_t1('-' * 30)
  165. self.insert_to_t1(f'开始合并视频---------------')
  166. if (self.spider.mix_video()):
  167. self.insert_to_t1(f'清理临时视频文件完成---------------')
  168. self.insert_to_t1(f'清理临时音频文件完成---------------')
  169. self.insert_to_t1(f'合并视频完成---------------')
  170. else:
  171. self.insert_to_t1(f'发生了异常错误!---------------')
  172. if not download_more:
  173. break
  174. self.b1.config(state=NORMAL)
  175. self.b2.config(state=NORMAL)
  176. def insert_to_t1(self,line,time_str=True):
  177. if time_str==True:
  178. time_string=My_Util().get_time_string()
  179. self.t1.insert(END,f'[{time_string}]'+line+'\n')
  180. else:
  181. self.t1.insert(END,line+'\n')
  182. self.t1.yview_moveto(1)
  183. def open_dir(self):
  184. abs_path = os.path.abspath(self.base_dir)
  185. # 使用绝对路径打开文件夹
  186. os.startfile(abs_path)
  187. def quit_window(self):
  188. ret = messagebox.askyesno('提示', '是否要退出?')
  189. if ret == True:
  190. self.window.destroy()
  191. def escape(self,event):
  192. self.quit_window()
  193. def connect_author(self):
  194. messagebox.showinfo('联系作者', '作者QQ:懷淰メ')
  195. def enter(self,event):
  196. self.thread_it(self.pre_analysis)
  197. def thread_it(self,func, *args):
  198. t = threading.Thread(target=func, args=args)
  199. self.window.update()
  200. t.setDaemon(True) # 设置守护,主线程结束,子线程结束
  201. t.start()
  202. if __name__ == '__main__':
  203. App()
  204. """
  205. test https://www.bilibili.com/video/BV1ML411J7es
  206. """

2.bilibili_video_spider

  1. import json
  2. import requests
  3. import re
  4. import os
  5. import subprocess
  6. from my_util import My_Util
  7. import time
  8. """
  9. 版本2分别下载音频和视频,通过ffmpeg合并
  10. 三种情况
  11. 1.单P
  12. 2.多P下载单集
  13. 3.多P下载全集
  14. """
  15. class Bibili_Video_Spider(object):
  16. def __init__(self,):
  17. self.s=requests.session()
  18. self.headers={
  19. 'Content-Range': 'bytes 0-xxxxxx',
  20. "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
  21. }
  22. self.util=My_Util()
  23. def set_start_url(self,start_url):
  24. self.start_url=start_url
  25. self.get_page_html()
  26. def get_video_title(self):
  27. """
  28. 起始视频标题,作为下载视频的目录名
  29. :return:
  30. """
  31. regx='name="keywords" content="(.*?),'
  32. title=re.findall(regx,self.srart_html)
  33. title=title[0]
  34. return title
  35. def get_page_html(self):
  36. """
  37. 获取网页源代码
  38. :return:
  39. """
  40. headers={
  41. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
  42. 'Content-Range': 'bytes 0-xxxxxx',
  43. 'Referer': self.start_url
  44. }
  45. r=self.s.get(self.start_url,headers=headers)
  46. if r.status_code==200:
  47. r.encoding='utf-8'
  48. self.srart_html=r.text
  49. def get_video_number(self):
  50. """
  51. 是否含有多P,若含有分P,则将所有分P名字存入list
  52. :return:
  53. """
  54. html_part = re.findall('window.__INITIAL_STATE__=(.*?)</script> <link rel="stylesheet"', self.srart_html)
  55. part_json_str = html_part[0].split(';(function(){var')[0]
  56. part_json = json.loads(part_json_str.strip())
  57. pages = part_json['videoData']['pages']
  58. self.part_name_list = [part_name['part'] for part_name in pages]
  59. if len(pages)!=1:
  60. part_number=len(pages)
  61. else:
  62. part_number=1
  63. return part_number
  64. def get_video_detail_json(self):
  65. """
  66. 获取视频详情json,里面包括视频m4a地址,以及audio音频,版本2主要依赖此Json
  67. :return:
  68. """
  69. regx='window.__playinfo__=(.*?)</script><script>window.__INITIAL_STATE'
  70. video_json_=re.findall(regx,self.srart_html)
  71. if video_json_:
  72. video_json=json.loads(video_json_[0])
  73. return video_json
  74. def get_video_and_audio(self,page_json,):
  75. """
  76. 获取视频的视频和音频,准备合并
  77. :param page_json:
  78. :return:
  79. """
  80. video_item={}
  81. video_data=[]
  82. data=page_json['data']
  83. video_definition_list = data.get('accept_description')
  84. video_detail_=data.get('dash').get('video')
  85. video_link_list=[]
  86. for video_detail__ in video_detail_:
  87. video_url=video_detail__.get('baseUrl')
  88. video_link_list.append(video_url)
  89. for v in zip(video_definition_list,video_link_list):
  90. item = {}
  91. item[v[0]]=v[1]
  92. video_data.append(item)
  93. video_item['video_length']=self.util.Convert_Millis(page_json["data"]['timelength'])
  94. video_item['audio_url'] = page_json["data"]["dash"]["audio"][0]["baseUrl"]
  95. video_item['video_detail'] = video_data
  96. return video_item
  97. def down_video(self,video_url):
  98. """
  99. 下载视频
  100. :param video_url:
  101. :param number: 分P的索引从1开始
  102. :return:
  103. """
  104. start_time=time.time()
  105. headers = {
  106. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
  107. 'Content-Range': 'bytes 0-xxxxxx',
  108. 'Referer': self.start_url
  109. }
  110. #下载视频
  111. r = self.s.get(video_url, stream=True, headers=headers)
  112. file_size=int(r.headers['Content-Length'])
  113. count=0
  114. with open(self.video_title+'-temp.mp4','wb')as f:
  115. for chunk in r.iter_content(chunk_size=1024):
  116. f.write(chunk)
  117. count+=len(chunk)
  118. progress=float(count/file_size*100)
  119. speed = My_Util().format_size((count) / (time.time() - start_time)) + '/S'
  120. yield progress,speed
  121. def downlonad_autio(self,audio_url):
  122. start_time=time.time()
  123. headers = {
  124. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
  125. 'Content-Range': 'bytes 0-xxxxxx',
  126. 'Referer': self.start_url
  127. }
  128. r = self.s.get(audio_url, stream=True, headers=headers)
  129. file_size=int(r.headers['Content-Length'])
  130. count=0
  131. with open(self.video_title+'-temp.aac','wb')as f:
  132. for chunk in r.iter_content(chunk_size=1024):
  133. f.write(chunk)
  134. count+=len(chunk)
  135. progress=float(count/file_size*100)
  136. speed = My_Util().format_size((count) / (time.time() - start_time)) + '/S'
  137. yield progress,speed
  138. def mix_video(self,):
  139. try:
  140. # print(f'开始合并{self.video_title}...')
  141. path = "ffmpeg.exe -i " + self.video_title + "-temp.mp4 -i " + self.video_title + "-temp.aac -vcodec copy -acodec copy " + self.video_title + ".mp4"
  142. subprocess.call(path, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  143. os.remove(self.video_title + "-temp.mp4")
  144. # print('[清理临时视频文件完成]...')
  145. os.remove(self.video_title + "-temp.aac")
  146. # print('[清理临时音频文件完成]...')
  147. return True
  148. except :
  149. return False

五.总结

  • 本次使用tkinter加ffmpeg实现了B站视频的下载,支持所选视频的所有画质的下载,tkinter完成GUI的搭建,实现交互,spider实现视频的解析与下载,ffmpeg实现视频与音频的合并,初步实现了B站视频的下载,当然这只是1.0版本,仍存在一些不足。



    1.代码逻辑混乱,复用率不高。(因为是分几天写成的,可能一些想法找不到了)。

    2.主要功能较少,GUI的优势没有明显凸显出来(当前的功能,打包成命令行也能轻易实现)。

    3.视频下载到了一个目录,应当将带有分P的全集视频,新建目录后下载(这里确实有点吹毛求疵,毕竟是1.0)。



    期待下一个版本

程序打包好放在了蓝奏云,欢迎各位试用思路、代码方面有什么不足欢迎各位大佬指正、批评!

python3GUI--实用!B站视频下载工具(附源码)的更多相关文章

  1. Web 开发中很实用的10个效果【附源码下载】

    在工作中,我们可能会用到各种交互效果.而这些效果在平常翻看文章的时候碰到很多,但是一时半会又想不起来在哪,所以养成知识整理的习惯是很有必要的.这篇文章给大家推荐10个在 Web 开发中很有用的效果,记 ...

  2. leaflet视频监控播放(附源码下载)

    前言 leaflet 入门开发系列环境知识点了解: leaflet api文档介绍,详细介绍 leaflet 每个类的函数以及属性等等 leaflet 在线例子 leaflet 插件,leaflet ...

  3. Android 音视频深入 四 录视频MP4(附源码下载)

    本篇项目地址,名字是<录音视频(有的播放器不能放,而且没有时长显示)>,求star https://github.com/979451341/Audio-and-video-learnin ...

  4. 转:Web 开发中很实用的10个效果【附源码下载】

    原文地址:http://www.cnblogs.com/lhb25/p/10-useful-web-effect.html 在工作中,我们可能会用到各种交互效果.而这些效果在平常翻看文章的时候碰到很多 ...

  5. Android 音视频深入 十 FFmpeg给视频加特效(附源码下载)

    项目地址,求starhttps://github.com/979451341/Audio-and-video-learning-materials/tree/master/FFmpeg(AVfilte ...

  6. Android 音视频深入 八 小视频录制(附源码下载)

    本篇项目地址,求starthttps://github.com/979451341/Audio-and-video-learning-materials/tree/master/%E5%B0%8F%E ...

  7. 原创SQlServer数据库生成简单的说明文档小工具(附源码)

    这是一款简单的数据库文档生成工具,主要实现了SQlServer生成说明文档的小工具,目前不够完善,主要可以把数据库的表以及表的详细字段信息,导出到 Word中,可以方便开发人员了解数据库的信息或写技术 ...

  8. 原创SQlServer数据库生成简单的说明文档包含(存储过程、视图、数据库批量备份)小工具(附源码)

    这是一款简单的数据库文档生成工具,主要实现了SQlServer生成说明文档的小工具,目前不够完善,主要可以把数据库的表以及表的详细字段信息,导出到 Word中,可以方便开发人员了解数据库的信息或写技术 ...

  9. 晓晨高效IP提取工具 附源码

    在网上找的几个代理ip网站,抓取下来的.解析网页用的是HtmlAgilityPack,没有用正则.自己重写了ListView使他动态加载的时候不闪烁.效果图 下载地址:http://files.cnb ...

  10. C#版Windows服务安装卸载小工具-附源码

    前言 在我们的工作中,经常遇到Windows服务的安装和卸载,在之前公司也普写过一个WinForm程序选择安装路径,这次再来个小巧灵活的控制台程序,不用再选择,只需放到需要安装服务的目录中运行就可以实 ...

随机推荐

  1. nginx 日志分析之 access.log 格式详解

    说明:access.log 的格式是可以自己自定义,输出的信息格式在nginx.conf中设置 一般默认配置如下: http { ... log_format main '$remote_addr - ...

  2. Cesium测量优化1

    简介:优化绘制点.线,面鼠标位置获取精度.支持3dties,gltf model,以及box等Geometry Entity上的位置拾取. 测试代码 <template> <div ...

  3. nginx转发端口路由器再转发

    场景 nginx 转发端口 路由器二次转发了,端口不一样 (shiro 或者其他一些权限控制架构会自动跳转,导致的端口不对.) proxy_set_header Host $host:$proxy_p ...

  4. 安装fearch

    sudo add-apt-repository ppa:christian-boxdoerfer/fsearch-daily sudo apt-get update sudo apt-get inst ...

  5. NC16644【字符串的展开】

    正确代码: #include <iostream>#include <algorithm>using namespace std;bool IsSame(char a, cha ...

  6. 金蝶K3无法查看关联信息

    场景: 某个用户点击采购订单界面--关联信息,界面显示正在加载,但是无法显示所有关联单据. 步骤: 1. 在其他电脑登录存在同样问题. 2. 其他模块可以正常显示 3. 删除该用户t_UserProf ...

  7. 如何在Debian10镜像中设置Nginx引擎模块

    目前,我们较多的服务器WEB环境都是用的Nginx引擎,我们采用服务器的目的是可以获取到更多的资源,而且建站数量是不受限制的.我们可以根据自己需要配置Nginx,可以自定义特定域的设置,允许您在单个服 ...

  8. 94、springboot+minio实现分片上传(超大文件快速上传)

    设计由来 在实际的项目开发中常遇到超大附件上传的情况,有时候客户会上传GB大小的文件,如果按照普通的MultipartFile方式来接收上传的文件,那么无疑会把服务器给干崩溃,更别说并发操作了.于是笔 ...

  9. APP的文件数据直传腾讯云COS实践

    简介 本文主要介绍基于腾讯云对象存储COS,如何快速实现一个app的文件直传功能.您的服务器上只需要生成和管理访问密钥,无需关心细节,文件数据都存放在腾讯云 COS 上. 架构说明 对于app应用,把 ...

  10. lg8862题解

    脑抽了,一开始想着扫描线然后用线段树求历史最大值.