曝肝三天,兩千行Python代碼,制作B站視頻下載工具(附源碼)


文章目錄

一.准備工作

二.預覽

  • 1.啟動
  • 2.解析
  • 3.下載中
  • 4.下載完成
  • 5.結果

三.設計流程

    • 1.bilibili_video_spider
    • 2.視頻json的查找

四.源代碼

    • 1.Bilibili_Video_Downloader-GUI
    • 2.bilibili_video_spider

五.總結

由於B站沒有PC客戶端,電腦下載視頻很不方便,遂使用Tk編寫一款B站視頻下載工具,輸入一個網址選擇清晰度之后就能夠下載對應的視頻,可以下載單P、合集、合集單P,使用可視化GUI圖形界面,交互性更強,來吧,展示~

很多人學習python,不知道從何學起。
很多人學習python,掌握了基本語法過后,不知道在哪里尋找案例上手。
很多已經做案例的人,卻不知道如何去學習更加高深的知識。
那么針對這三類人,我給大家提供一個好的學習平台,免費領取視頻教程,電子書籍,以及課程的源代碼!
QQ群:701698587
歡迎加入,一起討論 一起學習!

 

 

一.准備工作

tkinter、os系統模塊、re正則模塊、subprocess新的進程模塊、還有本次比較重要的ffmpeg.exe用於視頻和音頻的合並,關於ffmpeg請參考:ffmpeg - 百度百科

二.預覽

1.啟動

 

2.解析

 

 


解析出多個清晰度視頻以供下載

3.下載中

 

4.下載完成

 


分別下載完視頻和音頻后,對它們進行合並,最后輸出一個完整的視頻文件

5.結果

 


1080P+,針不戳

三.設計流程

1.bilibili_video_spider

 

2.視頻json的查找

  1. 首先查看網頁源代碼
  2. 在網頁的這個js里,能夠找到關於視頻的相關視頻、音頻、視頻質量、長度、格式…等信息,直接正則截取就好啦

 

緊接着,下面這個js里,就是視頻的aid、分P信息、up主信息、相關視頻推薦信息,也用正則就能截取

 

四.源代碼

1.Bilibili_Video_Downloader-GUI

from tkinter import *
from tkinter import ttk
from tkinter import messagebox
import os
import threading
from bilibili_video_spider import Bibili_Video_Spider as sp2
import re
from my_util import My_Util
"""
GUI+Spider
"""
class App:
    def __init__(self):
        self.base_dir = './bilibili_videos/'
        self.start_flag=''
        self.has_more_flag=''
        self.spider=sp2()
        self.create_widget()
        self.set_widget()
        self.place_widget()
        self.window.mainloop()

    def create_widget(self):
        self.window = Tk()
        self.window.title('Bilibili_Video_Downloader-v1.0')
        width = 450
        height = 520
        screen_width = self.window.winfo_screenwidth()
        screen_height = self.window.winfo_screenheight()
        left = (screen_width - width) / 2
        top = (screen_height - height) / 2
        self.window.geometry("%dx%d+%d+%d" % (width, height, left, top))
        self.window.resizable(0, 0)
        self.l1 = ttk.Label(self.window, text='請輸入視頻鏈接地址:')
        self.e1_var=StringVar()
        self.e1 = ttk.Entry(self.window, width=90,textvariable=self.e1_var)
        self.l5 = ttk.Label(self.window, text='選擇清晰度:')
        self.combobox=ttk.Combobox(self.window,state='readonly',width=15,justify='center')
        self.l2 = ttk.Label(self.window, text='當前狀態:')
        self.t1 = Text(self.window, width=80, height=20)
        self.l3_var=StringVar()
        self.l3 = ttk.Label(self.window, text='當前下載進度:',textvariable=self.l3_var)
        self.progress=ttk.Progressbar(self.window,orient=HORIZONTAL,length=400,mode='determinate',value=0,maximum=100)
        self.l4_var = StringVar()
        self.l4_var.set('0.0%[未下載]')
        self.l4 = ttk.Label(self.window, textvariable=self.l4_var)
        self.b1 = ttk.Button(self.window, text='解析', command=lambda: self.thread_it(self.pre_analysis))
        self.b2 = ttk.Button(self.window, text='下載', command=lambda: self.thread_it(self.donwload_video))

    def set_widget(self):
        self.window.protocol('WM_DELETE_WINDOW', self.quit_window)
        self.window.bind('<Escape>', self.escape)
        self.e1.bind('<Return>', self.enter)
        self.b2.config(state=DISABLED)
        self.combobox.config(value=['--請先解析--'])
        self.combobox.current(0)

    def place_widget(self):
        self.l1.pack(anchor="w")
        self.e1.pack(anchor="w", padx=20)
        self.l5.pack(anchor="w",pady=5)
        self.combobox.pack(anchor="center")
        self.l2.pack(anchor="w")
        self.t1.pack(anchor="w", padx=20)
        self.l3.pack(anchor="w",pady=5)
        self.progress.pack(pady=5)
        self.l4.pack()
        self.b1.pack(side='left', padx=90)
        self.b2.pack(side='left', padx=10)

    def pre_analysis(self):
        input_video_link = self.e1.get()
        input_video_link=input_video_link.strip()
        if input_video_link.startswith(r'https://www.bilibili.com/video/'):
            if '&' in input_video_link:
                raw_link=input_video_link.split('&')[0]
            else:
                raw_link=input_video_link
            try:
                #av 轉 bv
                av_number = int(re.findall('https://www.bilibili.com/video/av(\d+)?', raw_link)[0])
                url=raw_link.replace(av_number,My_Util().av_convert_bv(av_number))
            except IndexError:
                url=raw_link
            self.spider.set_start_url(url)
            self.spider.get_page_html()
            self.video_number = self.spider.get_video_number()
            base_title = self.spider.get_video_title()
            if re.match('https://www.bilibili.com/video/.*\?p=\d+',url):
                current_num=re.findall('https://www.bilibili.com/video/.*\?p=(\d+)',url)
                self.has_more_flag=True
                self.current_video_title=self.spider.part_name_list[int(current_num[0])]
            else:
                self.has_more_flag=False
                self.current_video_title=base_title
            self.entrace_url=url
            self.analysis_videos(url)
            if self.start_flag!=True:
                self.b2.config(state=NORMAL)
            # self.b1.config(state=DISABLED)
        else:
            messagebox.showwarning('警告', '請輸入正確的分享鏈接!')
            self.e1_var.set('')

    def analysis_videos(self,url):
        """
        :param url:
        :return:
        """
        My_Util().do_makedirs(self.base_dir)
        self.video_item_ = self.spider.get_video_and_audio(self.spider.get_video_detail_json())
        video_quality_list=[]
        for video_detail in self.video_item_['video_detail']:
            for data in video_detail.items():
                video_quality_list.append(data[0])
        self.combobox.config(value=video_quality_list)
        self.combobox.current(0)
        self.t1.delete(0.0,END)
        self.insert_to_t1(f'[視頻標題]:{self.current_video_title}')
        self.insert_to_t1(f'[視頻時長]:{self.video_item_["video_length"]}')
        self.insert_to_t1(f'[視頻清晰度]:{"  ".join(video_quality_list)}')
        self.insert_to_t1(f'請選擇清晰度后點擊下載按鈕---------------',time_str=False)

    def donwload_video(self):
        self.start_flag=True
        self.b2.config(state=DISABLED)
        if self.has_more_flag:
            ret = messagebox.askyesno('提示', '此視頻包含多P,是否下載全集?')
            if ret:
                download_more=True
            else:
                download_more=False
        else:
            download_more=False
        for i in range(self.video_number):
            if download_more:
                begin_url = self.entrace_url.split('?')[0] + f'?p={i+1}'
                self.spider.video_title = self.spider.part_name_list[i]
                current_title=self.spider.part_name_list[i]
            else:
                begin_url=self.entrace_url
                self.spider.video_title = self.current_video_title
                current_title =self.current_video_title
            self.insert_to_t1(f'開始下載{current_title}---------------')
            self.l3_var.set('視頻下載進度:')
            self.spider.set_start_url(begin_url)
            video_item_ = self.spider.get_video_and_audio(self.spider.get_video_detail_json())
            video_url_list=[]
            for video_detail in video_item_['video_detail']:
                for data in video_detail.items():
                    video_url_list.append(data[1])
            download_url = video_url_list[self.combobox.current()]
            current_video_name=self.spider.part_name_list[i]
            for progrees, speed in self.spider.down_video(download_url,):
                self.progress['value'] = progrees
                self.l4_var.set(f'進度:%.1f%% 速度:%s' % (progrees, speed))
                self.progress.update()
            self.insert_to_t1(f'[{current_video_name}視頻下載完成...')
            self.l4_var.set('100%[下載完成]')
            self.insert_to_t1('-' * 30)
            audio_url = video_item_['audio_url']
            self.insert_to_t1(f'開始下載{current_title}音頻---------------')
            self.l3_var.set('音頻下載進度:')
            for progrees, speed in self.spider.downlonad_autio(audio_url,):
                self.progress['value'] = progrees
                self.l4_var.set(f'進度:%.1f%% 速度:%s' % (progrees, speed))
                self.progress.update()
            self.insert_to_t1(f'[{current_video_name}音頻下載完成...')
            self.l4_var.set('100%[下載完成]')
            self.insert_to_t1('-' * 30)
            self.insert_to_t1(f'開始合並視頻---------------')
            if (self.spider.mix_video()):
                self.insert_to_t1(f'清理臨時視頻文件完成---------------')
                self.insert_to_t1(f'清理臨時音頻文件完成---------------')
                self.insert_to_t1(f'合並視頻完成---------------')
            else:
                self.insert_to_t1(f'發生了異常錯誤!---------------')
            if not download_more:
                break
        self.b1.config(state=NORMAL)
        self.b2.config(state=NORMAL)

    def insert_to_t1(self,line,time_str=True):
        if time_str==True:
            time_string=My_Util().get_time_string()
            self.t1.insert(END,f'[{time_string}]'+line+'\n')
        else:
            self.t1.insert(END,line+'\n')
        self.t1.yview_moveto(1)

    def open_dir(self):
        abs_path = os.path.abspath(self.base_dir)
        # 使用絕對路徑打開文件夾
        os.startfile(abs_path)

    def quit_window(self):
        ret = messagebox.askyesno('提示', '是否要退出?')
        if ret == True:
            self.window.destroy()

    def escape(self,event):
        self.quit_window()

    def connect_author(self):
        messagebox.showinfo('聯系作者', '作者QQ:懷淰メ')

    def enter(self,event):
        self.thread_it(self.pre_analysis)

    def thread_it(self,func, *args):
        t = threading.Thread(target=func, args=args)
        self.window.update()
        t.setDaemon(True)  # 設置守護,主線程結束,子線程結束
        t.start()

if __name__ == '__main__':
    App()
    """
    test         https://www.bilibili.com/video/BV1ML411J7es
    """

 

2.bilibili_video_spider

import json
import requests
import re
import os
import subprocess
from my_util import My_Util
import time

"""
版本2分別下載音頻和視頻,通過ffmpeg合並

三種情況
1.單P
2.多P下載單集
3.多P下載全集

"""
class Bibili_Video_Spider(object):
    def __init__(self,):
        self.s=requests.session()
        self.headers={
            'Content-Range': 'bytes 0-xxxxxx',
            "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"
        }
        self.util=My_Util()

    def set_start_url(self,start_url):
        self.start_url=start_url
        self.get_page_html()

    def get_video_title(self):
        """
        起始視頻標題,作為下載視頻的目錄名
        :return:
        """
        regx='name="keywords" content="(.*?),'
        title=re.findall(regx,self.srart_html)
        title=title[0]
        return title

    def get_page_html(self):
        """
        獲取網頁源代碼
        :return:
        """
        headers={
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
        'Content-Range': 'bytes 0-xxxxxx',
        'Referer': self.start_url
    }
        r=self.s.get(self.start_url,headers=headers)
        if r.status_code==200:
            r.encoding='utf-8'
            self.srart_html=r.text

    def get_video_number(self):
        """
        是否含有多P,若含有分P,則將所有分P名字存入list
        :return:
        """
        html_part = re.findall('window.__INITIAL_STATE__=(.*?)</script> <link rel="stylesheet"', self.srart_html)
        part_json_str = html_part[0].split(';(function(){var')[0]
        part_json = json.loads(part_json_str.strip())
        pages = part_json['videoData']['pages']
        self.part_name_list = [part_name['part'] for part_name in pages]
        if len(pages)!=1:
            part_number=len(pages)

        else:
            part_number=1
        return part_number

    def get_video_detail_json(self):
        """
        獲取視頻詳情json,里面包括視頻m4a地址,以及audio音頻,版本2主要依賴此Json
        :return:
        """
        regx='window.__playinfo__=(.*?)</script><script>window.__INITIAL_STATE'
        video_json_=re.findall(regx,self.srart_html)
        if video_json_:
            video_json=json.loads(video_json_[0])
            return video_json

    def get_video_and_audio(self,page_json,):
        """
        獲取視頻的視頻和音頻,准備合並
        :param page_json:
        :return:
        """
        video_item={}
        video_data=[]
        data=page_json['data']
        video_definition_list = data.get('accept_description')
        video_detail_=data.get('dash').get('video')
        video_link_list=[]
        for video_detail__ in video_detail_:
            video_url=video_detail__.get('baseUrl')
            video_link_list.append(video_url)
        for v in zip(video_definition_list,video_link_list):
            item = {}
            item[v[0]]=v[1]
            video_data.append(item)
        video_item['video_length']=self.util.Convert_Millis(page_json["data"]['timelength'])
        video_item['audio_url'] = page_json["data"]["dash"]["audio"][0]["baseUrl"]
        video_item['video_detail'] = video_data
        return video_item

    def down_video(self,video_url):
        """
        下載視頻
        :param video_url:
        :param number: 分P的索引從1開始
        :return:
        """
        start_time=time.time()
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
            'Content-Range': 'bytes 0-xxxxxx',
            'Referer': self.start_url
        }
        #下載視頻
        r = self.s.get(video_url, stream=True, headers=headers)
        file_size=int(r.headers['Content-Length'])
        count=0
        with open(self.video_title+'-temp.mp4','wb')as f:
            for chunk in r.iter_content(chunk_size=1024):
                f.write(chunk)
                count+=len(chunk)
                progress=float(count/file_size*100)
                speed = My_Util().format_size((count) / (time.time() - start_time)) + '/S'
                yield progress,speed

    def downlonad_autio(self,audio_url):
        start_time=time.time()
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
            'Content-Range': 'bytes 0-xxxxxx',
            'Referer': self.start_url
        }
        r = self.s.get(audio_url, stream=True, headers=headers)
        file_size=int(r.headers['Content-Length'])
        count=0
        with open(self.video_title+'-temp.aac','wb')as f:
            for chunk in r.iter_content(chunk_size=1024):
                f.write(chunk)
                count+=len(chunk)
                progress=float(count/file_size*100)
                speed = My_Util().format_size((count) / (time.time() - start_time)) + '/S'
                yield progress,speed

    def mix_video(self,):
        try:
            # print(f'開始合並{self.video_title}...')
            path = "ffmpeg.exe -i " + self.video_title + "-temp.mp4 -i " + self.video_title + "-temp.aac -vcodec copy -acodec copy " + self.video_title + ".mp4"
            subprocess.call(path, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            os.remove(self.video_title + "-temp.mp4")
            # print('[清理臨時視頻文件完成]...')
            os.remove(self.video_title + "-temp.aac")
            # print('[清理臨時音頻文件完成]...')
            return True
        except :
            return False

 

五.總結

  • 本次使用tkinter加ffmpeg實現了B站視頻的下載,支持所選視頻的所有畫質的下載,tkinter完成GUI的搭建,實現交互,spider實現視頻的解析與下載,ffmpeg實現視頻與音頻的合並,初步實現了B站視頻的下載,當然這只是1.0版本,仍存在一些不足。


1.代碼邏輯混亂,復用率不高。(因為是分幾天寫成的,可能一些想法找不到了)。
2.主要功能較少,GUI的優勢沒有明顯凸顯出來(當前的功能,打包成命令行也能輕易實現)。
3.視頻下載到了一個目錄,應當將帶有分P的全集視頻,新建目錄后下載(這里確實有點吹毛求疵,畢竟是1.0)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM