Skip to main content

第十课:JS逆向-B站

对于B站这些视频平台,视频往往不会以一整个文件返回

下面这段代码,在 jump_url 中输入某个B站视频的URL 就可以下载这个视频

import time
import requests
import json
import re
import os
from lxml import etree
from moviepy.editor import *



file_path=r"C:\Users\28121\Desktop\coding-learning\爬虫学习\爬取资源" 
if not os.path.exists(file_path):  # 检查地址是否存在
    os.mkdir(file_path) 

# 在jump_url 中填入想要的视频地址
jump_url='https://www.bilibili.com/video/BV1ZH4y1w7yJ/?spm_id_from=333.337.search-card.all.click&vd_source=e59e065fb52ab5a6c6accaf0d793c23b'
headers = {
    'Accept': '*/*',
    'Accept-Language': 'en-US,en;q=0.5',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36'
}


response = requests.get(jump_url, headers=headers)    #response.text 包含了网页的 HTML 源代码。

# 正则表达式查找页面中 __playinfo__ 的内容,(.*?) 匹配括号内的 JSON 数据。
match = re.search( '__playinfo__=(.*?)</script><script>',response.text)   
playinfo =  json.loads(match.group(1))  

# 正则表达式查找页面中 __INITIAL_STATE__ 的 JSON 数据。包括视频标题、视频作者、弹幕等基本信息。
match = re.search( r'__INITIAL_STATE__=(.*?);\(function\(\)',response.text)  
basic_info = json.loads(match.group(1))


video_url = playinfo['data']['dash']['video'][0]['baseUrl']  # 视频分多种格式,直接取分辨率最高的视频 1080p
audio_url = playinfo['data']['dash']['audio'][0]['baseUrl']  # 取出音频地址
title = basic_info['videoData']['title']
print('视频名字:',title)
print('视频地址:', video_url)
print('音频地址:', audio_url)


# 更新请求头和合成路径
headers.update({"Referer": jump_url})   #添加 Referer 头,模拟从视频页面跳转到视频文件 URL 的行为,避免服务器拒绝访问
video_name=f'{title}_video.mp4'
video_path=os.path.join(file_path, video_name)
audio_name=f'{title}_audio.mp4'
audio_path=os.path.join(file_path, audio_name)


# 下载视频和音频
with open(video_path, 'ab') as output:   # 模式 'ab' 表示以 追加二进制.
        video = requests.get(video_url, headers=headers)   
        output.write(video.content)
with open(audio_path, 'ab') as output:
        audio = requests.get(audio_url, headers=headers)
        output.write(audio.content)
print('视频下载完成')


# 合成视频和音频
print('原始视频音频合并中,请耐心等待~')
vd = VideoFileClip(video_path)
ad = AudioFileClip(audio_path)
vd2 = vd.set_audio(ad)  # 将提取到的音频和视频文件进行合成
output = video_path.replace('_video','')
vd2.write_videofile(output)  # 输出新的视频文件

# 移除原始的视频和音频
os.remove(video_path)
os.remove(audio_path)
print('视频合成完成')

对于视频这种大文件,我们其实建议采用分块下载

# 分块下载视频
headers.update({"Referer": jump_url})   #添加 Referer 头,模拟从视频页面跳转到视频文件 URL 的行为,避免服务器拒绝访问
video_content = requests.get(video_url, headers=headers)   #检测 content-length(文件总大小)。
received_video = 0
video_name=f'{title}_video.mp4'
video_path=os.path.join(file_path, video_name)


with open(video_path, 'ab') as output:
    while int(video_content.headers['content-length']) > received_video:  #判断文件是否完全下载
        # content-length:服务器返回的文件总大小(以字节为单位)。
        # received_video:已下载字节数
        headers['Range'] = 'bytes=' + str(received_video) + '-'   #每次请求从 received_video 开始,下载剩余部分的数据。
        # HTTP Range 头 用于请求文件的特定字节范围。例如:
        # bytes=0-499:请求第 0 到 499 个字节的数据。
        # bytes=500-:从第 500 个字节开始下载,直到文件结束。
        response = requests.get(video_url, headers=headers)   # 服务器会根据 Range 返回指定范围的文件数据。
        output.write(response.content)
        received_video += len(response.content)

为什么选择分块下载而不是一次性下载?

  1. 一次性下载会将整个文件加载到内存中。比如一个 1GB 的视频文件会占用至少 1GB 内存。可能导致内存不足或程序崩溃。
  2. 如果下载中断(如网络波动、程序退出),分块下载可以从中断的地方继续,而不用重新下载整个文件。
  3. 可以控制下载速度

虽然这段代码并未明确规定块的大小,但仍然实现了分块下载的效果

因为这里每次块的大小由服务器控制,而非代码

如果服务器允许大文件下载,这段代码看起来像是整体下载。

如果服务器限制每次响应的大小(例如按 4MB 块分发),则实现了分块下载。


为了让分块下载更精确,可以明确设定每个块的大小

chunk_size = 1024 * 1024  # 1MB 块大小
while int(video_content.headers['content-length']) > received_video:
    end_byte = received_video + chunk_size - 1  # 当前块的结束字节
    headers['Range'] = f'bytes={received_video}-{end_byte}'
    
    response = requests.get(video_url, headers=headers)
    output.write(response.content)
    
    received_video += len(response.content)