课程视频抓取与下载技能
通用型课程视频下载方案,适配任意课程平台(腾讯课堂、B站、超星学习通、网易云课堂、学堂在线、小鹅通等)。
核心限制
- 无法录屏:浏览器自动化没有录屏能力,但通过抓取视频源文件直链下载,画质更好、速度更快
- DRM 加密视频无法下载:如果平台使用 blob URL、Widevine/DRM 加密流,只能建议用户用 OBS 手动录屏
- 不同平台适配程度不同:需要根据页面结构动态分析
工作流程
第一步:接收链接 & 打开页面
用户提供建议课程链接后,用 browser 导航打开:
import sys, os
sys.path.insert(0, os.path.join(os.getenv("SKILL_PATH"), "browser", "scripts"))
import browser
result = browser.navigate(url="用户提供的链接")
print(result)
第二步:检测登录墙
分析页面快照中的文本和交互元素,判断是否需要登录:
判断标准(命中任一即触发):
- 页面文本包含"登录""注册""请先登录""登录后观看"等字样
- 存在登录按钮、登录表单(用户名/密码输入框)
- 视频区域显示"登录后即可观看"或类似提示
- 页面跳转到了登录页
如果需要登录:
- 停止浏览器操作,告诉用户遇到了登录要求
- 调用
ask_user_question,提供选项:「弹出浏览器我自己操作」/「放弃」 - 用户选择接管后,调用
browser.request_manual()弹窗 - 立刻再次调用
ask_user_question询问用户是否完成操作 - 用户确认完成后,调用
browser.get_interactive_elements()读取当前页面快照,继续后续流程
第三步:分析课程页面结构
登录成功后,分析页面结构:
- 课程目录/章节列表:找到课程章节、课时列表的元素索引
- 视频播放区域:定位
<video>标签或 iframe 嵌入 - 章节展开:如果章节默认折叠,需要逐个点击展开
使用 browser.execute_script() 提取页面关键信息:
# 提取所有 video 标签的 src
result = browser.execute_script("""
const videos = document.querySelectorAll('video');
return Array.from(videos).map(v => ({
src: v.src || v.currentSrc,
sources: Array.from(v.querySelectorAll('source')).map(s => s.src)
}));
""")
print(result)
# 提取所有 iframe(有些平台视频嵌在 iframe 中)
result = browser.execute_script("""
const iframes = document.querySelectorAll('iframe');
return Array.from(iframes).map(f => ({ src: f.src, id: f.id }));
""")
print(result)
第四步:提取视频直链
根据页面情况,采用不同策略提取视频地址:
策略 A:直接 video 标签
- 如果
<video>标签有直接的 src(mp4/webm/ogg),直接获取
策略 B:m3u8 流
- 通过 Network 请求拦截或页面 JS 变量中查找 m3u8 地址
result = browser.execute_script("""
// 尝试从全局变量和常见的播放器对象中提取
const results = [];
// 检查 window 上的视频相关变量
for (const key of Object.keys(window)) {
try {
const val = String(window[key]);
if (val.includes('.m3u8') || val.includes('.mp4')) {
const match = val.match(/https?:\\/\\/[^"'\\s]+\\.(m3u8|mp4)[^"'\\s]*/);
if (match) results.push(match[0]);
}
} catch(e) {}
}
// 检查所有 script 标签中的视频地址
document.querySelectorAll('script').forEach(s => {
const matches = s.textContent.match(/https?:\\/\\/[^"'\\s]+\\.(m3u8|mp4)[^"'\\s]*/g);
if (matches) results.push(...matches);
});
return [...new Set(results)];
""")
print(result)
策略 C:XHR/Fetch 拦截
- 注入网络请求拦截器,等待视频数据加载后捕获请求 URL
# 注入 fetch 拦截器
browser.execute_script("""
window.__videoUrls = [];
const origFetch = window.fetch;
window.fetch = function(...args) {
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
if (url.includes('.m3u8') || url.includes('.mp4') || url.includes('video')) {
window.__videoUrls.push(url);
}
return origFetch.apply(this, args);
};
const origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
if (url.includes('.m3u8') || url.includes('.mp4') || url.includes('video')) {
window.__videoUrls.push(url);
}
return origOpen.apply(this, arguments);
};
""")
策略 D:进入 iframe 内部提取
- 如果视频嵌在 iframe 中,需要进入 iframe 的文档上下文提取
第五步:下载视频
获取到视频直链后,使用 Python 下载:
import requests
import os
def download_video(url, save_path, filename):
"""下载视频文件"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': '当前课程页面URL', # 部分平台需要正确的 Referer
}
# m3u8 需要 ffmpeg 下载
if url.endswith('.m3u8'):
download_m3u8(url, save_path, filename)
return
# mp4 直接下载
resp = requests.get(url, headers=headers, stream=True)
total = int(resp.headers.get('content-length', 0))
downloaded = 0
filepath = os.path.join(save_path, filename)
with open(filepath, 'wb') as f:
for chunk in resp.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total > 0:
progress = downloaded / total * 100
print(f"\r下载进度: {progress:.1f}% ({downloaded}/{total})", end="")
print(f"\n下载完成: {filepath}")
m3u8 下载(需要 ffmpeg):
def download_m3u8(m3u8_url, save_path, filename):
"""使用 ffmpeg 下载 m3u8 视频流"""
import subprocess
filepath = os.path.join(save_path, filename)
# 先检查 ffmpeg 是否可用
try:
subprocess.run(['ffmpeg', '-version'], capture_output=True, check=True)
except FileNotFoundError:
print("未安装 ffmpeg,正在尝试用 requests 直接合并 ts 片段...")
download_m3u8_fallback(m3u8_url, save_path, filename)
return
cmd = [
'ffmpeg', '-i', m3u8_url,
'-c', 'copy', '-bsf:a', 'aac_adtstoasc',
'-y', filepath
]
subprocess.run(cmd, check=True)
print(f"下载完成: {filepath}")
第六步:批量处理课程章节
如果课程有多个章节/课时:
- 先提取课程目录结构和所有章节链接
- 按顺序逐个进入章节页面
- 对每个页面执行视频提取和下载
- 下载完成后回到目录页,进入下一章节
- 记录已下载列表,支持断点续传
# 批量处理示例伪代码
chapters = extract_chapter_list() # 从页面提取章节列表
downloaded = [] # 已下载记录
for i, chapter in enumerate(chapters):
if chapter['title'] in downloaded:
continue # 跳过已下载的
print(f"[{i+1}/{len(chapters)}] 正在处理: {chapter['title']}")
# 进入章节页面
browser.click(element_index=chapter['element_index'])
# 等待视频加载
import time
time.sleep(3)
# 提取视频链接
video_url = extract_video_url()
if video_url:
download_video(video_url, save_dir, f"{chapter['title']}.mp4")
downloaded.append(chapter['title'])
else:
print(f" 未找到视频链接,可能需要特殊处理")
# 返回课程目录
browser.navigate(back_to_course_url)
输出规范
- 所有视频保存到工作区下的
output/course_videos/目录 - 文件命名格式:
{序号}_{章节标题}.mp4 - 生成一份下载记录文件
download_log.txt,记录每个文件的下载状态 - 如果某些视频因 DRM 或其他原因无法下载,在记录中标注原因
常见平台适配要点
| 平台 | 特点 | 注意事项 | |------|------|----------| | 腾讯课堂 | 视频嵌在 iframe,m3u8 流 | 需要 cookie 鉴权,先登录再抓 | | B站 | BV号直链,有 CDN | 可用 bili-api 或直接抓取 | | 超星学习通 | iframe + object 嵌入 | 需要进入 iframe 上下文提取 | | 网易云课堂 | m3u8 加密流 | 可能有防盗链,需要正确 Referer | | 小鹅通 | 微信生态,m3u8 | 部分需要微信授权 | | 学堂在线 | HLS 流 | 需要 cookie |
错误处理
- blob URL:视频使用 blob URL 时无法直接下载,提示用户使用 OBS 录屏
- DRM 加密:检测到 Widevine/PlayReady 等加密时,提示用户无法绕过
- 防盗链:下载失败时尝试添加正确的 Referer 和 Cookie
- 网络中断:支持断点续传(记录已下载进度)
- 超时:单个视频下载超时 30 分钟则跳过,记录到日志
使用示例
用户只需说:
- "帮我下载这个课程的视频:[链接]"
- "抓取这个网课的视频"
- "录制这个链接里的课程视频"
Agent 自动执行完整流程。
微信扫一扫