当前位置: 首页 > news >正文

小电视视频内容获取GUI工具

提示:视频下载请遵守B站用户协议,仅下载获得授权的公开内容

本工具是一个基于Python开发的桌面应用程序,使用ttkbootstrap库构建现代UI界面。该工具允许用户通过GUI界面轻松下载B站视频,自动解析视频流和音频流,并使用FFmpeg进行合并处理。
由于B站的视频分为视频内容和音频内容,合并后才是用户所观看到的内容,所以需要用到FFmpeg。
具体的分析过程太过赘述,本文不过多描述,需要具体了解分析过程,同行皆有分析,请自行搜索查看。

核心功能

  • 智能解析:提取B站视频标题和音视频流地址
  • 双线程下载:并行下载视频流和音频流
  • 进度监控:实时显示下载进度百分比
  • 日志系统:完整记录操作和下载过程
  • FFmpeg集成:自动合并音视频流为完整MP4文件

FFmpeg依赖配置详解

为什么需要FFmpeg?

B站视频采用音视频分离的传输方式:

  1. 视频流(不含音频)
  2. 音频流(单独文件)
  3. FFmpeg负责将两者合并为完整MP4文件

安装配置步骤

Windows系统
# 1. 下载官方编译版
https://www.gyan.dev/ffmpeg/builds/
# 2. 解压到本地目录(建议C:\ffmpeg)
# 3. 添加环境变量
系统属性 → 高级 → 环境变量 → Path → 添加:
C:\ffmpeg\bin
# 4. 验证安装
cmd输入:ffmpeg -version
macOS系统
# 通过Homebrew安装
brew install ffmpeg
# 验证安装
ffmpeg -version
Linux系统
# Ubuntu/Debian
sudo apt update
sudo apt install ffmpeg
# CentOS/Fedora
sudo yum install ffmpeg

项目中的FFmpeg调用逻辑

# 关键代码片段
output_path = os.path.join(save_path, f"{filename}_完整版.mp4")
cmd = f'ffmpeg -i "{mp4_path}" -i "{mp3_path}" -c:v copy -c:a copy "{output_path}" -loglevel quiet'
result = os.system(cmd)

参数解析

参数作用必要性
-c:v copy直接复制视频流(不重新编码)必需
-c:a copy直接复制音频流(不重新编码)必需
-loglevel quiet抑制控制台输出可选

项目使用指南

  1. 安装Python依赖:
pip install requests ttkbootstrap lxml
  1. 运行程序:
python Bilibili_GUI_Pro.py
  1. 界面操作:
    • 粘贴B站视频URL
    • 选择保存路径
    • 点击"开始下载"

具体代码

"""
#!/usr/bin/env python3
# --*-- coding:UTF-8 --*--
@Author : LuoQiu
@Software : PyCharm
@File : Bilibili_GUI_Pro.py
@Time : 2025/08/13 12:05:07
"""
import os
import re
import json
import threading
import requests
import tkinter as tk
from tkinter import filedialog, messagebox
from lxml import etree
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap.scrolled import ScrolledTextclass BilibiliDownloader(ttk.Window):def __init__(self):super().__init__(title="B站视频下载器", themename="cosmo", resizable=(True, True))self.geometry("800x600")self.minsize(700, 500)self.iconbitmap(default="")# 设置默认保存路径self.save_path = os.path.join(os.path.expanduser("~"), "Videos", "Bilibili")if not os.path.exists(self.save_path):os.makedirs(self.save_path, exist_ok=True)# 创建变量self.url_var = tk.StringVar()self.path_var = tk.StringVar(value=self.save_path)self.is_downloading = Falseself.download_thread = None# 创建界面self.create_widgets()self.center_window()def center_window(self):"""将窗口居中显示"""self.update_idletasks()width = self.winfo_width()height = self.winfo_height()x = (self.winfo_screenwidth() // 2) - (width // 2)y = (self.winfo_screenheight() // 2) - (height // 2)self.geometry(f"{width}x{height}+{x}+{y}")def create_widgets(self):"""创建GUI组件"""# 创建主框架main_frame = ttk.Frame(self, padding=10)main_frame.pack(fill=BOTH, expand=YES)# 顶部标题title_label = ttk.Label(main_frame,text="B站视频下载器",font=("Helvetica", 18, "bold"),bootstyle="primary")title_label.pack(pady=10)# 输入框架input_frame = ttk.LabelFrame(main_frame, text="下载设置", padding=10)input_frame.pack(fill=X, pady=10)# URL输入url_frame = ttk.Frame(input_frame)url_frame.pack(fill=X, pady=5)url_label = ttk.Label(url_frame, text="视频URL:", width=10)url_label.pack(side=LEFT, padx=5)url_entry = ttk.Entry(url_frame, textvariable=self.url_var, bootstyle="primary")url_entry.pack(side=LEFT, fill=X, expand=YES, padx=5)paste_button = ttk.Button(url_frame,text="粘贴",command=self.paste_url,bootstyle="info-outline",width=8)paste_button.pack(side=LEFT, padx=5)# 保存路径path_frame = ttk.Frame(input_frame)path_frame.pack(fill=X, pady=5)path_label = ttk.Label(path_frame, text="保存路径:", width=10)path_label.pack(side=LEFT, padx=5)path_entry = ttk.Entry(path_frame, textvariable=self.path_var, bootstyle="primary")path_entry.pack(side=LEFT, fill=X, expand=YES, padx=5)browse_button = ttk.Button(path_frame,text="浏览",command=self.browse_path,bootstyle="info-outline",width=8)browse_button.pack(side=LEFT, padx=5)# 按钮框架button_frame = ttk.Frame(main_frame)button_frame.pack(fill=X, pady=10)self.download_button = ttk.Button(button_frame,text="开始下载",command=self.start_download,bootstyle="success",width=15)self.download_button.pack(side=LEFT, padx=5)self.cancel_button = ttk.Button(button_frame,text="取消下载",command=self.cancel_download,bootstyle="danger",state=DISABLED,width=15)self.cancel_button.pack(side=LEFT, padx=5)clear_button = ttk.Button(button_frame,text="清空日志",command=self.clear_log,bootstyle="warning",width=15)clear_button.pack(side=LEFT, padx=5)about_button = ttk.Button(button_frame,text="关于",command=self.show_about,bootstyle="info",width=15)about_button.pack(side=RIGHT, padx=5)# 进度条框架progress_frame = ttk.Frame(main_frame)progress_frame.pack(fill=X, pady=5)self.progress_var = ttk.IntVar(value=0)self.progress_label = ttk.Label(progress_frame, text="下载进度: 0%")self.progress_label.pack(side=LEFT, padx=5)self.progress_bar = ttk.Progressbar(progress_frame,variable=self.progress_var,bootstyle="success-striped",length=100,mode="determinate",maximum=100)self.progress_bar.pack(side=LEFT, fill=X, expand=YES, padx=5)# 日志框架log_frame = ttk.LabelFrame(main_frame, text="下载日志", padding=10)log_frame.pack(fill=BOTH, expand=YES, pady=10)self.log_text = ScrolledText(log_frame, padding=5, height=6, autohide=True, bootstyle="primary")self.log_text.pack(fill=BOTH, expand=YES)# 状态栏status_frame = ttk.Frame(main_frame)status_frame.pack(fill=X, side=BOTTOM, pady=5)self.status_label = ttk.Label(status_frame, text="就绪", bootstyle="secondary")self.status_label.pack(side=LEFT)# 版权信息copyright_label = ttk.Label(status_frame, text="© 2025 B站视频下载器", bootstyle="secondary")copyright_label.pack(side=RIGHT)def paste_url(self):"""粘贴剪贴板内容到URL输入框"""try:clipboard_text = self.clipboard_get()self.url_var.set(clipboard_text)except:self.log("剪贴板中没有可用内容")def browse_path(self):"""选择保存路径"""path = filedialog.askdirectory(initialdir=self.path_var.get())if path:self.path_var.set(path)def log(self, message):"""向日志窗口添加消息"""self.log_text.insert(tk.END, f"{message}\n")self.log_text.see(tk.END)self.update_idletasks()def clear_log(self):"""清空日志窗口"""self.log_text.delete(1.0, tk.END)def show_about(self):"""显示关于信息"""messagebox.showinfo("关于B站视频下载器","B站视频下载器 v1.0\n\n""这是一个使用Python和ttkbootstrap开发的B站视频下载工具。\n""可以下载B站视频并自动合并音视频。\n\n""© 2025 B站视频下载器")def update_status(self, message):"""更新状态栏信息"""self.status_label.config(text=message)def update_progress(self, percent, message=None):"""更新进度条"""self.progress_var.set(percent)if message:self.progress_label.config(text=f"下载进度: {message}")else:self.progress_label.config(text=f"下载进度: {percent}%")def toggle_buttons(self, is_downloading):"""切换按钮状态"""if is_downloading:self.download_button.config(state=DISABLED)self.cancel_button.config(state=NORMAL)else:self.download_button.config(state=NORMAL)self.cancel_button.config(state=DISABLED)def start_download(self):"""开始下载视频"""url = self.url_var.get().strip()if not url:messagebox.showerror("错误", "请输入有效的B站视频URL")returnif not url.startswith("https://www.bilibili.com"):messagebox.showerror("错误", "请输入有效的B站视频URL")returnsave_path = self.path_var.get()if not os.path.exists(save_path):try:os.makedirs(save_path, exist_ok=True)except Exception as e:messagebox.showerror("错误", f"无法创建保存目录: {str(e)}")returnself.is_downloading = Trueself.toggle_buttons(True)self.update_status("正在下载...")self.update_progress(0, "准备中...")# 创建并启动下载线程self.download_thread = threading.Thread(target=self.download_video, args=(url, save_path))self.download_thread.daemon = Trueself.download_thread.start()def cancel_download(self):"""取消下载"""if self.is_downloading:self.is_downloading = Falseself.log("正在取消下载...")self.update_status("已取消")self.update_progress(0, "已取消")self.toggle_buttons(False)def download_video(self, url, save_path):"""在线程中下载视频"""try:# 模拟浏览器发送请求headers = {'referer': 'https://www.bilibili.com','User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/''106.0.0.0 Safari/537.36 Edg/106.0.1370.47'}self.log(f"开始下载: {url}")self.log("正在获取视频信息...")self.after(0, lambda: self.update_progress(5, "获取视频信息..."))# 获取爬取视频地址r = requests.get(url, headers=headers)if not self.is_downloading:return# 解析视频地址,截取关键信息try:info = re.findall('window.__playinfo__=(.*?)</script>', r.text)[0]video_url = json.loads(info)["data"]["dash"]["video"][0]['baseUrl']audio_url = json.loads(info)["data"]["dash"]["audio"][0]['baseUrl']# 获取视频名称html = etree.HTML(r.text)filename = html.xpath('//h1/text()')[0]# 替换文件名中的非法字符filename = re.sub(r'[\\/:*?"<>|]', "_", filename)self.log(f"视频标题: {filename}")self.after(0, lambda: self.update_progress(10, "解析完成"))except Exception as e:self.log(f"解析视频信息失败: {str(e)}")self.update_status("下载失败")self.after(0, lambda: self.update_progress(0, "下载失败"))self.toggle_buttons(False)self.is_downloading = Falsereturnif not self.is_downloading:return# 下载视频部分self.log("正在下载视频部分...")self.after(0, lambda: self.update_progress(20, "下载视频..."))try:response = requests.get(video_url, headers=headers, stream=True)# 获取文件大小file_size = int(response.headers.get('Content-Length', 0))downloaded = 0chunks = []for chunk in response.iter_content(chunk_size=1024 * 1024):  # 每次读取1MBif not self.is_downloading:returnif chunk:chunks.append(chunk)downloaded += len(chunk)# 计算视频下载进度(占总进度的30%)progress = int(20 + (downloaded / file_size) * 30) if file_size > 0 else 30self.after(0, lambda p=progress: self.update_progress(p,f"视频: {int((downloaded / file_size) * 100)}%" if file_size > 0 else "视频下载中..."))video_content = b''.join(chunks)except Exception as e:self.log(f"下载视频失败: {str(e)}")self.after(0, lambda: self.update_progress(0, "下载失败"))self.update_status("下载失败")self.is_downloading = Falsereturnif not self.is_downloading:return# 下载音频部分self.log("正在下载音频部分...")self.after(0, lambda: self.update_progress(50, "下载音频..."))try:response = requests.get(audio_url, headers=headers, stream=True)# 获取文件大小file_size = int(response.headers.get('Content-Length', 0))downloaded = 0chunks = []for chunk in response.iter_content(chunk_size=1024 * 1024):  # 每次读取1MBif not self.is_downloading:returnif chunk:chunks.append(chunk)downloaded += len(chunk)# 计算音频下载进度(占总进度的20%)progress = int(50 + (downloaded / file_size) * 20) if file_size > 0 else 60self.after(0, lambda p=progress: self.update_progress(p,f"音频: {int((downloaded / file_size) * 100)}%" if file_size > 0 else "音频下载中..."))audio_content = b''.join(chunks)except Exception as e:self.log(f"下载音频失败: {str(e)}")self.after(0, lambda: self.update_progress(0, "下载失败"))self.update_status("下载失败")self.is_downloading = Falsereturnif not self.is_downloading:return# 保存视频和音频mp4_path = os.path.join(save_path, f"{filename}.mp4")mp3_path = os.path.join(save_path, f"{filename}.mp3")output_path = os.path.join(save_path, f"{filename}_完整版.mp4")self.log("正在保存视频部分...")self.after(0, lambda: self.update_progress(70, "保存视频..."))with open(mp4_path, 'wb') as f:f.write(video_content)if not self.is_downloading:if os.path.exists(mp4_path):os.remove(mp4_path)returnself.log("正在保存音频部分...")self.after(0, lambda: self.update_progress(75, "保存音频..."))with open(mp3_path, 'wb') as f:f.write(audio_content)if not self.is_downloading:if os.path.exists(mp4_path):os.remove(mp4_path)if os.path.exists(mp3_path):os.remove(mp3_path)return# 合并视频和音频self.log("正在合并音视频...")self.after(0, lambda: self.update_progress(80, "合并音视频..."))cmd = f'ffmpeg -i "{mp4_path}" -i "{mp3_path}" -c:v copy -c:a copy "{output_path}" -loglevel quiet'result = os.system(cmd)if result != 0:self.log("合并失败,请确保已安装ffmpeg并添加到环境变量")self.log("视频和音频文件已保存,您可以使用其他工具手动合并")self.update_status("合并失败")self.after(0, lambda: self.update_progress(0, "合并失败"))else:self.log("合并成功,正在清理临时文件...")self.after(0, lambda: self.update_progress(90, "清理临时文件..."))# 删除临时文件os.remove(mp4_path)os.remove(mp3_path)self.log(f"下载完成!文件保存在: {output_path}")self.after(0, lambda: self.update_progress(100, "下载完成"))# 在主线程中显示完成消息self.after(0, lambda: messagebox.showinfo("下载完成",f"视频 '{filename}' 已成功下载!\n\n保存位置: {output_path}"))self.update_status("下载完成")except Exception as e:self.log(f"下载过程中出错: {str(e)}")self.update_status("下载出错")finally:self.is_downloading = Falseself.after(0, lambda: self.toggle_buttons(False))if __name__ == "__main__":app = BilibiliDownloader()app.mainloop()

运行效果

效果展示


在这里插入图片描述

在这里插入图片描述

http://www.lryc.cn/news/620207.html

相关文章:

  • Ansible 实操笔记:Playbook 与变量管理
  • 传输层协议 TCP(1)
  • C语言队列的实现
  • 浪浪山小妖怪电影
  • HarmonyOS 开发实战:搞定应用名字与图标更换,全流程可运行示例
  • 《卷积神经网络(CNN):解锁视觉与多模态任务的深度学习核心》
  • 从 VLA 到 VLM:低延迟RTSP|RTMP视频链路在多模态AI中的核心角色与工程实现
  • AI驱动的前端革命:10项颠覆性技术如何在LibreChat中融为一体
  • Java19 Integer 位操作精解:compress与expand《Hacker‘s Delight》(第二版,7.4节)
  • Docker部署RAGFlow:开启Kibana查询ES数据指南
  • 学习嵌入式的第十九天——Linux——文件编程
  • 如何生成.patch?
  • 开发Excel Add-in的心得笔记
  • Redis ubuntu下载Redis的C++客户端
  • 3分钟 Spring AI 实现对话功能
  • 二次筛法Quadratic Sieve因子分解法----C语言实现
  • 【MCP开发】Nodejs+Typescript+pnpm+Studio搭建Mcp服务
  • 每日五个pyecharts可视化图表-line:从入门到精通 (5)
  • 物联网之小白调试网关设备
  • 《算法导论》第 23 章 - 最小生成树
  • PyTorch基础(Numpy与Tensor)
  • 可搜索的 HTML 版本 Emoji 图标大全,可以直接打开网页使用,每个图标可以点击复制,方便使用
  • Mac安装ant
  • WPS文字和Word文档如何选择多个不连续的行、段
  • Date/Calendar/DateFormat/LocalDate
  • Linux中备份的练习
  • element-ui 时间线(timeLine)内容分成左右两侧
  • 数据分析小白训练营:基于python编程语言的Numpy库介绍(第三方库)(下篇)
  • 车载软件架构 --- MCU刷写擦除相关疑问?
  • 《红黑树驱动的Map/Set实现:C++高效关联容器全解析》