1. 项目概述当自动化遇上加密流媒体如果你也曾在深夜对着学习通里那个永远转圈加载的视频任务点感到绝望或者为了凑够那该死的观看时长而不得不把手机挂在一边播放那么你一定能理解我们为什么要折腾这个“校园自动化任务系统”的第四部分。这不仅仅是为了“解放双手”更是一场与平台规则和技术壁垒的正面较量。今天我们就来深度解剖学习通视频任务的核心——加密的m3u8流媒体并一步步实现从解密、下载到最终完成“任务点”的完整自动化流程。简单来说学习通上的视频内容尤其是课程视频普遍采用了基于HTTP Live Streaming (HLS) 的加密传输技术。你看到的不是一个可以直接下载的.mp4文件而是一个名为.m3u8的索引文件它像一份菜单列出了成百上千个被加密分割的.ts视频切片。平台通过动态密钥和复杂的请求头验证确保只有其官方客户端App或网页在正常登录状态下才能流畅播放。我们的目标就是模拟这个“正常播放”的行为并让它自动化、可脚本化。这涉及到网络抓包分析、加密算法逆向、请求模拟等一系列操作绝非简单的“点击播放”那么简单。接下来我将以一个真实的实战案例带你走通这条充满挑战但成就感十足的技术路径。2. 核心思路与技术选型解析2.1 为什么是m3u8以及它为何棘手HLSHTTP Live Streaming是苹果公司提出的流媒体协议因其良好的自适应码率和兼容性被国内外众多在线教育、视频平台广泛采用。学习通使用的保利威等第三方服务商也基于此协议。一个典型的加密m3u8流程是这样的当你点击播放时客户端首先获取一个主m3u8文件里面可能包含多种清晰度的流如720p, 480p。选择某一清晰度后会得到另一个m3u8文件这个文件里每一行都是一个.ts切片文件的URL并且附带了加密信息最常见的是#EXT-X-KEY标签指明了密钥的获取方式URI和加密方法通常是AES-128。棘手之处在于动态密钥密钥URI往往不是一个静态链接而是包含令牌token或会话ID有效期极短且需要携带特定的Cookie或Authorization头去请求。请求验证除了密钥请求.ts切片本身也可能需要验证Referer、User-Agent甚至自定义头部否则返回403/404。链路复杂从登录、获取课程列表、进入章节、触发视频播放请求到最终拿到可用的m3u8中间可能有多次重定向和状态依赖。风控升级随着自动化脚本的增多平台增加了如滑块验证码尤其在频繁请求或异地登录时、行为检测鼠标移动、点击频率等风控手段。2.2 我们的技术栈与工具选型面对上述挑战一个稳定可靠的自动化方案需要分层处理。以下是经过实战检验的技术选型网络分析层浏览器开发者工具 Fiddler/Charles开发者工具F12主要用于初步分析网页结构、XHR/Fetch请求查看常规的请求头、响应体。对于学习通网页版这是第一步。Fiddler/Charles这是重中之重的抓包工具。因为学习通客户端包括Electron封装的桌面版的流量、以及可能存在的非HTTP协议如WebSocket通信都需要这类代理工具才能完整捕获。特别是获取m3u8和密钥请求的完整链路必须在此工具中设置代理并安装证书后才能看到。请求模拟与自动化核心Python 相关库requests/httpx/aiohttp用于发送HTTP请求。requests同步简单稳定httpx支持HTTP/2有时对某些服务器更友好aiohttp用于异步并发下载数百个.ts切片能极大提升效率。browser_cookie3一个神器可以直接从你电脑的浏览器Chrome, Firefox中提取已登录状态的Cookie避免手动模拟登录的复杂流程尤其是面对图形验证码或滑块时。前提是你已经用浏览器手动成功登录了学习通。CryptoPython的加密库用于AES解密.ts切片。我们需要使用从密钥URI获取到的密钥以CBC或AES-128模式解密下载的切片。m3u8一个专门解析m3u8文件的库能方便地提取出切片URL列表和#EXT-X-KEY信息。selenium/playwright当纯HTTP请求模拟遇到无法逾越的障碍时如复杂的JavaScript渲染、无法绕过的滑块验证我们需要动用浏览器自动化工具。它们可以真实地操作浏览器完成登录、跳转到课程页面等动作然后我们再从浏览器上下文中提取关键的Cookie或Token。这通常作为保底方案。媒体处理层FFmpeg这是最终将解密后的.ts切片合并、转码为单个.mp4文件的终极工具。通过命令行调用稳定高效。注意整个流程的合法性边界必须清晰。我们的技术讨论仅限于个人学习、研究网络协议和技术实现原理以及为完成自己已选修课程的合法学习任务提供自动化辅助。任何用于干扰平台正常运营、侵犯他人课程内容版权、或进行未经授权的批量抓取的行为都是不当且可能违法的。3. 实战解剖一步步获取并处理加密m3u83.1 第一步捕获关键网络请求这是最需要耐心和技巧的一步。我们以学习通网页版为例。环境准备打开Fiddler确保其捕获HTTPS流量的证书已安装并信任。在Fiddler设置中允许远程计算机连接Tools - Options - Connections这有时对某些客户端抓包有用。浏览器代理设置将浏览器的代理设置为Fiddler的默认地址通常是127.0.0.1:8888。登录与触发在设置好代理的浏览器中正常登录学习通进入你的课程找到目标视频章节点击播放按钮。筛选与分析在Fiddler的会话列表中你会看到大量请求。我们需要找到关键的几个寻找m3u8在右侧的Inspectors标签页查看请求的URL或响应体。使用CtrlF搜索.m3u8关键词。你可能会先找到一个“主m3u8”其URL可能包含playback.m3u8或index.m3u8等字样。定位密钥请求找到具体的视频流m3u8后查看其响应内容。你会看到类似下面的内容#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-KEY:METHODAES-128,URIhttps://xxx.com/key.php?tokenabc123,IV0x... #EXTINF:10.000, https://xxx.com/segment0.ts #EXTINF:10.000, https://xxx.com/segment1.ts ...这里的URI就是密钥地址。在Fiddler中查找向这个URI发起的请求这就是密钥请求。复制请求信息分别对视频流m3u8的请求和密钥请求在Fiddler中右键选择Copy - Copy Headers (Only)或Copy - Copy Session (as cURL)。这将为我们后续的Python脚本提供最真实的请求头模板。3.2 第二步Python脚本模拟请求与解析拿到cURL命令或请求头后我们开始编写Python代码。import re import m3u8 import requests from Crypto.Cipher import AES import browser_cookie3 # 1. 获取浏览器Cookie前提浏览器已登录学习通 # 以Chrome为例 cj browser_cookie3.chrome(domain_name.chaoxing.com) # 学习通主域名 cookie_dict {cookie.name: cookie.value for cookie in cj} headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., # 从抓包信息中复制 Referer: https://mooc1.chaoxing.com/mycourse/studentstudy?, # 关键必须与抓包时一致 # 其他必要的头部如 Authorization, Custom-Header 等 } # 2. 请求m3u8文件 m3u8_url 你抓取到的视频流m3u8地址 resp requests.get(m3u8_url, headersheaders, cookiescookie_dict) m3u8_content resp.text # 3. 解析m3u8 playlist m3u8.loads(m3u8_content) if playlist.keys and playlist.keys[0]: key_info playlist.keys[0] key_uri key_info.uri key_method key_info.method iv key_info.iv if key_info.iv else None print(f找到加密密钥URI: {key_uri}, 方法: {key_method}) # 4. 请求密钥 key_resp requests.get(key_uri, headersheaders, cookiescookie_dict) if key_resp.status_code 200: encryption_key key_resp.content # 密钥通常是16字节的二进制数据 print(成功获取加密密钥) else: print(f获取密钥失败: {key_resp.status_code}) exit() else: print(该m3u8未加密或使用不支持的加密方式) encryption_key None iv None # 5. 准备解密器如果是AES-128 if encryption_key and key_method AES-128: # 注意IV的处理有时IV在m3u8中指定有时是切片的序列号 if iv: cipher AES.new(encryption_key, AES.MODE_CBC, iviv) else: # 如果没有指定IV有时默认使用全零IV但更常见的是使用媒体序列号作为IV # 需要根据实际情况调整这是一个常见的坑 cipher AES.new(encryption_key, AES.MODE_CBC, ivb\x00*16)实操心得Referer头至关重要很多服务器的防盗链策略基于此。务必使用从抓包中复制的准确值。密钥请求的URL可能也携带了动态参数如token,t等这些参数可能来源于之前某个请求的响应或者是JavaScript生成的。如果直接请求失败需要回溯查找这些参数的来源。browser_cookie3并非万能。如果学习通使用了HttpOnly的Cookie或更复杂的会话管理此方法可能失效。此时需要考虑用selenium进行完整的登录流程然后从WebDriver中获取Cookie。3.3 第三步并发下载与解密TS切片获取密钥和切片列表后下载数百个.ts文件是另一个性能瓶颈。我们使用aiohttp进行异步下载。import aiohttp import asyncio from urllib.parse import urljoin import os async def download_and_decrypt_ts(session, ts_url, index, key, cipher, base_path): 异步下载单个TS切片并解密 try: async with session.get(ts_url, headersheaders) as resp: if resp.status 200: encrypted_data await resp.read() # 解密 if cipher: # AES CBC模式要求数据长度是16的倍数需要处理填充 decrypted_data cipher.decrypt(encrypted_data) # PKCS7填充去除常见 padding_len decrypted_data[-1] decrypted_data decrypted_data[:-padding_len] else: decrypted_data encrypted_data # 保存解密后的文件 filename os.path.join(base_path, fsegment_{index:04d}.ts) with open(filename, wb) as f: f.write(decrypted_data) print(f已下载并解密: {index}) return filename else: print(f下载失败 {ts_url}: {resp.status}) return None except Exception as e: print(f处理 {ts_url} 时出错: {e}) return None async def download_all_segments(segments_uri, key, cipher, base_path./ts_files): 并发下载所有切片 if not os.path.exists(base_path): os.makedirs(base_path) # 构建完整的TS URL列表m3u8中的URI可能是相对路径 base_url /.join(segments_uri[0].split(/)[:-1]) / if segments_uri else ts_urls [urljoin(base_url, seg.uri) for seg in playlist.segments] async with aiohttp.ClientSession(cookiescookie_dict, headersheaders) as session: tasks [] for idx, ts_url in enumerate(ts_urls): task asyncio.create_task(download_and_decrypt_ts(session, ts_url, idx, key, cipher, base_path)) tasks.append(task) # 控制并发数避免请求过快被封 results await asyncio.gather(*tasks, return_exceptionsTrue) return [r for r in results if r and not isinstance(r, Exception)] # 运行异步任务 loop asyncio.get_event_loop() downloaded_files loop.run_until_complete(download_all_segments(playlist.segments, encryption_key, cipher))注意事项并发控制不要一次性发起成百上千个并发请求这极易触发服务器的速率限制或直接被封IP。可以使用asyncio.Semaphore来限制最大并发数例如设置为10-20。错误重试网络请求可能失败必须为每个下载任务添加重试机制如tenacity库。IV动态计算对于AES-128 CBC模式如果m3u8中没有指定IV且不是全零IV那么IV可能是每个切片媒体序列号#EXT-X-MEDIA-SEQUENCE 切片索引的某种转换如大端序的16字节。这需要仔细分析客户端JavaScript的解密逻辑是最深的水坑之一。3.4 第四步合并与转码所有.ts文件下载解密后它们是独立的。我们需要将其合并。# 方法一使用FFmpeg命令行最可靠 # 首先创建一个文件列表文件 filelist_path os.path.join(base_path, filelist.txt) with open(filelist_path, w, encodingutf-8) as f: for file in sorted(downloaded_files): # 确保按顺序 f.write(ffile {os.path.abspath(file)}\n) import subprocess output_mp4 final_video.mp4 # 使用FFmpeg的concat demuxer进行合并 cmd [ ffmpeg, -f, concat, -safe, 0, -i, filelist_path, -c, copy, # 直接流复制不重新编码速度极快 output_mp4 ] subprocess.run(cmd, checkTrue) print(f视频已合并至: {output_mp4}) # 方法二如果FFmpeg不可用可以纯Python二进制合并但可能无法处理编码问题 # with open(output_mp4, wb) as outfile: # for file in sorted(downloaded_files): # with open(file, rb) as infile: # outfile.write(infile.read())提示-c copy参数意味着“流复制”它只是将TS文件的音视频流拼接起来不进行耗时的重新编码因此速度非常快。但如果TS文件之间的编码参数有不一致极少见可能会导致播放问题此时可以去掉-c copy让FFmpeg重新编码-c:v libx264 -c:a aac但耗时将大大增加。4. 自动化完成“任务点”的终极挑战下载视频只是第一步我们的终极目标是让系统自动完成学习通上的“任务点”即标记视频已观看。这比下载要困难得多因为它涉及到与学习通前端逻辑的深度交互。4.1 任务点完成的机制分析通过抓包分析“正常播放完成”的过程你会发现心跳包视频播放期间网页或App会以固定间隔如每30秒或每分钟向一个特定API发送“心跳”请求上报当前播放位置、时长等信息。完成请求当视频播放到末尾或达到一定比例如95%会触发一个“任务点完成”的POST请求。参数依赖这些请求携带大量参数如clazzId班级ID、courseId课程ID、jobid任务ID、objectId对象ID即视频资源ID、playingTime播放时长、duration视频总长等。其中许多ID需要从课程页面源码或之前的API响应中提取。状态验证服务器会验证你的登录状态、任务是否开启、当前播放进度是否合理等。4.2 模拟完成请求的Python实现思路是先通过请求获取任务状态信息然后构造心跳和完成请求。# 假设我们已经从课程页面解析出了以下关键ID clazz_id 1234567 course_id 7654321 job_id 998877 object_id video_abc123 # 对应具体视频资源的ID user_id 888999 # 从Cookie或用户信息接口获取 # 1. 获取视频总时长可以从m3u8的#EXTINF累加或从另一个API获取 total_duration 1200 # 假设总时长1200秒 # 2. 模拟心跳请求每30秒发送一次 heartbeat_url https://mooc1.chaoxing.com/multimedia/log/a for current_time in range(0, total_duration 60, 30): # 多发送一次确保覆盖 heartbeat_data { clazzId: clazz_id, courseId: course_id, jobid: job_id, objectId: object_id, playingTime: current_time, duration: total_duration, otherInfo: , userid: user_id, isdrag: 0, view: pc, enc: 计算出的加密字符串, # 注意这个enc字段通常是关键需要逆向JS计算 } # 注意实际的enc字段计算非常复杂是学习通主要的反爬手段。 # 它可能由多个字段拼接后经过特定的MD5或AES加密生成。 # 必须通过分析player.js等前端文件找到加密函数并模拟。 resp requests.post(heartbeat_url, dataheartbeat_data, headersheaders, cookiescookie_dict) print(f心跳 {current_time}s: {resp.status_code}) time.sleep(0.5) # 适当延迟模拟真人 # 3. 模拟完成任务点请求 finish_url https://mooc1.chaoxing.com/job/multimedia/jobFinish finish_data { clazzId: clazz_id, courseId: course_id, jobid: job_id, objectId: object_id, playingTime: total_duration, # 播放到总时长 duration: total_duration, userid: user_id, isdrag: 3, # 完成状态码 view: pc, enc: 计算出的完成请求enc, # 同样需要计算 } finish_resp requests.post(finish_url, datafinish_data, headersheaders, cookiescookie_dict) if finish_resp.status_code 200 and finish_resp.json().get(isPassed): print(任务点已完成) else: print(任务点完成失败:, finish_resp.text)最大的难点——enc参数 这个字段是学习通反自动化脚本的核心。你需要使用浏览器开发者工具的“源代码”面板全局搜索enc、jobFinish、multimedia/log等关键词找到负责生成这个参数的JavaScript函数。通常是一个复杂的函数将多个参数按特定顺序拼接然后进行MD5哈希或者使用AES加密后再Base64编码。你需要使用Python的execjs库或PyExecJS来执行这段JavaScript代码或者更彻底地将它的逻辑用Python重写。4.3 应对滑块验证码等风控当你频繁发送请求或从非常用IP登录时可能会触发滑块验证码。识别请求返回特定状态码如403或响应内容中包含验证码相关关键词如verify、slide。应对策略降低频率在心跳请求间增加随机延迟模拟人类观看的不规律性。维持会话确保Cookie长期有效避免频繁重新登录。使用Selenium应对当检测到需要滑块时切换到Selenium自动化浏览器执行以下步骤from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time # 启动浏览器访问出现滑块的页面 driver webdriver.Chrome() driver.get(https://passport2.chaoxing.com/...) # 滑块验证页面URL # 定位滑块和缺口图片这需要分析页面结构 slide_block WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, slideBlock)) ) gap_bg driver.find_element(By.ID, gapBg) # 计算需要滑动的距离通过图片像素比对这是另一个复杂课题 # distance calculate_distance(gap_bg.screenshot_as_png, slide_block.screenshot_as_png) # 模拟人类滑动轨迹先加速后减速 # drag_and_drop(slide_block, distance) # 等待验证通过获取新的Cookie time.sleep(3) new_cookies driver.get_cookies() # 将new_cookies更新到requests的session中自动识别滑块缺口和模拟滑动轨迹是一个专门的领域可以使用图像识别库如opencv-python计算距离并用动作链模拟滑动。但这部分代码不稳定且平台会更新验证码样式。5. 常见问题与排查技巧实录在实战中你会遇到各种各样的问题。下面是一个快速排查指南问题现象可能原因排查步骤与解决方案获取m3u8返回403/4041. 请求头不完整或错误。2. Referer或Origin校验失败。3. Cookie过期或无效。4. URL带有时效性参数已过期。1.对比抓包用Fiddler抓取一次成功请求逐字段对比你的Python请求头特别是User-Agent,Referer,Accept-*。2.更新Cookie重新从浏览器获取Cookie或检查Cookie中关键会话字段如fid,UID是否存在。3.检查URLm3u8的URL可能包含t,token等参数需要确认其生成逻辑并正确获取。获取密钥返回403或空数据1. 密钥请求需要额外的认证头如Authorization: Bearer xxx。2. 密钥URI本身也受Referer保护。3. 密钥已过期动态密钥有效期很短。1.分析密钥请求在Fiddler中仔细查看成功的密钥请求看是否有自定义头部。2.模拟播放器行为尝试在获取m3u8后立即请求密钥不要有延迟。3.关联请求密钥的token可能来源于获取m3u8请求的响应头或响应体中的某个字段。下载的TS切片解密后无法播放1.密钥错误获取的密钥内容不对。2.IV错误AES CBC模式的初始化向量(IV)不正确。3.解密模式错误可能不是AES-128 CBC或者是CBC模式但填充方式不对。4.TS文件损坏下载不完整。1.验证密钥用抓包工具查看密钥请求的原始响应Hex视图确认是16字节二进制数据。2.测试IV尝试使用m3u8中指定的IV、全零IV、或用序列号计算的IV分别解密第一个切片用播放器或ffprobe测试哪个能播。3.检查填充尝试PKCS7、PKCS5或无填充。解密后末尾字节如果是规律的如0x01,0x02 0x02很可能就是填充。4.重新下载确保下载完整无网络错误。心跳或完成请求返回“参数错误”或“enc校验失败”1.enc字段计算错误。2. 请求参数缺失或格式不对。3. 服务器端风控。1.逆向JS这是最核心的一步。在浏览器Sources面板中搜索enc生成函数用Python复现其逻辑。重点检查参数拼接顺序、使用的哈希算法MD5, SHA1、是否进行了Base64或Hex编码。2.参数对比用Python脚本生成的数据包和浏览器发送的数据包进行逐字段对比。3.添加冗余参数有些参数看似无用但服务器会校验必须和浏览器发送的一模一样。触发滑块验证码1. 请求频率过高。2. IP地址异常。3. 行为模式不像真人。1.立即暂停停止当前脚本的所有请求。2.切换IP/账号如果可能更换网络环境或使用备用账号。3.引入随机延迟在后续脚本中在请求间加入time.sleep(random.uniform(1, 5))。4.升级为浏览器自动化对于必须完成的任务使用Selenium等工具手动或半自动地处理滑块。FFmpeg合并后音画不同步1. TS切片的时间戳不连续。2. 视频流和音频流的编码参数在中间发生变化。1.使用FFmpeg重新编码不要用-c copy改用-c:v libx264 -c:a aac让FFmpeg重新处理时间戳。2.检查源用ffprobe检查几个TS切片看它们的codec_type和time_base是否一致。不一致的源文件很难完美合并。最后的个人体会构建这样一个自动化系统其过程本身就是一个绝佳的学习项目它迫使你去深入理解HTTP协议、加密解密、前端逆向和反爬虫策略。然而必须清醒认识到这是一场“道高一尺魔高一丈”的博弈。平台的防御策略在不断升级今天的有效方法明天可能就会失效。因此你的代码需要具备良好的可维护性和适应性将易变的逻辑如URL构造、enc计算抽象成配置或独立的模块。更重要的是始终将技术的应用约束在合理的个人学习辅助范畴内尊重平台规则和知识产权。毕竟技术是工具如何使用它取决于你的初衷。
HLS加密流媒体自动化处理实战:从m3u8解密到任务完成
1. 项目概述当自动化遇上加密流媒体如果你也曾在深夜对着学习通里那个永远转圈加载的视频任务点感到绝望或者为了凑够那该死的观看时长而不得不把手机挂在一边播放那么你一定能理解我们为什么要折腾这个“校园自动化任务系统”的第四部分。这不仅仅是为了“解放双手”更是一场与平台规则和技术壁垒的正面较量。今天我们就来深度解剖学习通视频任务的核心——加密的m3u8流媒体并一步步实现从解密、下载到最终完成“任务点”的完整自动化流程。简单来说学习通上的视频内容尤其是课程视频普遍采用了基于HTTP Live Streaming (HLS) 的加密传输技术。你看到的不是一个可以直接下载的.mp4文件而是一个名为.m3u8的索引文件它像一份菜单列出了成百上千个被加密分割的.ts视频切片。平台通过动态密钥和复杂的请求头验证确保只有其官方客户端App或网页在正常登录状态下才能流畅播放。我们的目标就是模拟这个“正常播放”的行为并让它自动化、可脚本化。这涉及到网络抓包分析、加密算法逆向、请求模拟等一系列操作绝非简单的“点击播放”那么简单。接下来我将以一个真实的实战案例带你走通这条充满挑战但成就感十足的技术路径。2. 核心思路与技术选型解析2.1 为什么是m3u8以及它为何棘手HLSHTTP Live Streaming是苹果公司提出的流媒体协议因其良好的自适应码率和兼容性被国内外众多在线教育、视频平台广泛采用。学习通使用的保利威等第三方服务商也基于此协议。一个典型的加密m3u8流程是这样的当你点击播放时客户端首先获取一个主m3u8文件里面可能包含多种清晰度的流如720p, 480p。选择某一清晰度后会得到另一个m3u8文件这个文件里每一行都是一个.ts切片文件的URL并且附带了加密信息最常见的是#EXT-X-KEY标签指明了密钥的获取方式URI和加密方法通常是AES-128。棘手之处在于动态密钥密钥URI往往不是一个静态链接而是包含令牌token或会话ID有效期极短且需要携带特定的Cookie或Authorization头去请求。请求验证除了密钥请求.ts切片本身也可能需要验证Referer、User-Agent甚至自定义头部否则返回403/404。链路复杂从登录、获取课程列表、进入章节、触发视频播放请求到最终拿到可用的m3u8中间可能有多次重定向和状态依赖。风控升级随着自动化脚本的增多平台增加了如滑块验证码尤其在频繁请求或异地登录时、行为检测鼠标移动、点击频率等风控手段。2.2 我们的技术栈与工具选型面对上述挑战一个稳定可靠的自动化方案需要分层处理。以下是经过实战检验的技术选型网络分析层浏览器开发者工具 Fiddler/Charles开发者工具F12主要用于初步分析网页结构、XHR/Fetch请求查看常规的请求头、响应体。对于学习通网页版这是第一步。Fiddler/Charles这是重中之重的抓包工具。因为学习通客户端包括Electron封装的桌面版的流量、以及可能存在的非HTTP协议如WebSocket通信都需要这类代理工具才能完整捕获。特别是获取m3u8和密钥请求的完整链路必须在此工具中设置代理并安装证书后才能看到。请求模拟与自动化核心Python 相关库requests/httpx/aiohttp用于发送HTTP请求。requests同步简单稳定httpx支持HTTP/2有时对某些服务器更友好aiohttp用于异步并发下载数百个.ts切片能极大提升效率。browser_cookie3一个神器可以直接从你电脑的浏览器Chrome, Firefox中提取已登录状态的Cookie避免手动模拟登录的复杂流程尤其是面对图形验证码或滑块时。前提是你已经用浏览器手动成功登录了学习通。CryptoPython的加密库用于AES解密.ts切片。我们需要使用从密钥URI获取到的密钥以CBC或AES-128模式解密下载的切片。m3u8一个专门解析m3u8文件的库能方便地提取出切片URL列表和#EXT-X-KEY信息。selenium/playwright当纯HTTP请求模拟遇到无法逾越的障碍时如复杂的JavaScript渲染、无法绕过的滑块验证我们需要动用浏览器自动化工具。它们可以真实地操作浏览器完成登录、跳转到课程页面等动作然后我们再从浏览器上下文中提取关键的Cookie或Token。这通常作为保底方案。媒体处理层FFmpeg这是最终将解密后的.ts切片合并、转码为单个.mp4文件的终极工具。通过命令行调用稳定高效。注意整个流程的合法性边界必须清晰。我们的技术讨论仅限于个人学习、研究网络协议和技术实现原理以及为完成自己已选修课程的合法学习任务提供自动化辅助。任何用于干扰平台正常运营、侵犯他人课程内容版权、或进行未经授权的批量抓取的行为都是不当且可能违法的。3. 实战解剖一步步获取并处理加密m3u83.1 第一步捕获关键网络请求这是最需要耐心和技巧的一步。我们以学习通网页版为例。环境准备打开Fiddler确保其捕获HTTPS流量的证书已安装并信任。在Fiddler设置中允许远程计算机连接Tools - Options - Connections这有时对某些客户端抓包有用。浏览器代理设置将浏览器的代理设置为Fiddler的默认地址通常是127.0.0.1:8888。登录与触发在设置好代理的浏览器中正常登录学习通进入你的课程找到目标视频章节点击播放按钮。筛选与分析在Fiddler的会话列表中你会看到大量请求。我们需要找到关键的几个寻找m3u8在右侧的Inspectors标签页查看请求的URL或响应体。使用CtrlF搜索.m3u8关键词。你可能会先找到一个“主m3u8”其URL可能包含playback.m3u8或index.m3u8等字样。定位密钥请求找到具体的视频流m3u8后查看其响应内容。你会看到类似下面的内容#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-KEY:METHODAES-128,URIhttps://xxx.com/key.php?tokenabc123,IV0x... #EXTINF:10.000, https://xxx.com/segment0.ts #EXTINF:10.000, https://xxx.com/segment1.ts ...这里的URI就是密钥地址。在Fiddler中查找向这个URI发起的请求这就是密钥请求。复制请求信息分别对视频流m3u8的请求和密钥请求在Fiddler中右键选择Copy - Copy Headers (Only)或Copy - Copy Session (as cURL)。这将为我们后续的Python脚本提供最真实的请求头模板。3.2 第二步Python脚本模拟请求与解析拿到cURL命令或请求头后我们开始编写Python代码。import re import m3u8 import requests from Crypto.Cipher import AES import browser_cookie3 # 1. 获取浏览器Cookie前提浏览器已登录学习通 # 以Chrome为例 cj browser_cookie3.chrome(domain_name.chaoxing.com) # 学习通主域名 cookie_dict {cookie.name: cookie.value for cookie in cj} headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., # 从抓包信息中复制 Referer: https://mooc1.chaoxing.com/mycourse/studentstudy?, # 关键必须与抓包时一致 # 其他必要的头部如 Authorization, Custom-Header 等 } # 2. 请求m3u8文件 m3u8_url 你抓取到的视频流m3u8地址 resp requests.get(m3u8_url, headersheaders, cookiescookie_dict) m3u8_content resp.text # 3. 解析m3u8 playlist m3u8.loads(m3u8_content) if playlist.keys and playlist.keys[0]: key_info playlist.keys[0] key_uri key_info.uri key_method key_info.method iv key_info.iv if key_info.iv else None print(f找到加密密钥URI: {key_uri}, 方法: {key_method}) # 4. 请求密钥 key_resp requests.get(key_uri, headersheaders, cookiescookie_dict) if key_resp.status_code 200: encryption_key key_resp.content # 密钥通常是16字节的二进制数据 print(成功获取加密密钥) else: print(f获取密钥失败: {key_resp.status_code}) exit() else: print(该m3u8未加密或使用不支持的加密方式) encryption_key None iv None # 5. 准备解密器如果是AES-128 if encryption_key and key_method AES-128: # 注意IV的处理有时IV在m3u8中指定有时是切片的序列号 if iv: cipher AES.new(encryption_key, AES.MODE_CBC, iviv) else: # 如果没有指定IV有时默认使用全零IV但更常见的是使用媒体序列号作为IV # 需要根据实际情况调整这是一个常见的坑 cipher AES.new(encryption_key, AES.MODE_CBC, ivb\x00*16)实操心得Referer头至关重要很多服务器的防盗链策略基于此。务必使用从抓包中复制的准确值。密钥请求的URL可能也携带了动态参数如token,t等这些参数可能来源于之前某个请求的响应或者是JavaScript生成的。如果直接请求失败需要回溯查找这些参数的来源。browser_cookie3并非万能。如果学习通使用了HttpOnly的Cookie或更复杂的会话管理此方法可能失效。此时需要考虑用selenium进行完整的登录流程然后从WebDriver中获取Cookie。3.3 第三步并发下载与解密TS切片获取密钥和切片列表后下载数百个.ts文件是另一个性能瓶颈。我们使用aiohttp进行异步下载。import aiohttp import asyncio from urllib.parse import urljoin import os async def download_and_decrypt_ts(session, ts_url, index, key, cipher, base_path): 异步下载单个TS切片并解密 try: async with session.get(ts_url, headersheaders) as resp: if resp.status 200: encrypted_data await resp.read() # 解密 if cipher: # AES CBC模式要求数据长度是16的倍数需要处理填充 decrypted_data cipher.decrypt(encrypted_data) # PKCS7填充去除常见 padding_len decrypted_data[-1] decrypted_data decrypted_data[:-padding_len] else: decrypted_data encrypted_data # 保存解密后的文件 filename os.path.join(base_path, fsegment_{index:04d}.ts) with open(filename, wb) as f: f.write(decrypted_data) print(f已下载并解密: {index}) return filename else: print(f下载失败 {ts_url}: {resp.status}) return None except Exception as e: print(f处理 {ts_url} 时出错: {e}) return None async def download_all_segments(segments_uri, key, cipher, base_path./ts_files): 并发下载所有切片 if not os.path.exists(base_path): os.makedirs(base_path) # 构建完整的TS URL列表m3u8中的URI可能是相对路径 base_url /.join(segments_uri[0].split(/)[:-1]) / if segments_uri else ts_urls [urljoin(base_url, seg.uri) for seg in playlist.segments] async with aiohttp.ClientSession(cookiescookie_dict, headersheaders) as session: tasks [] for idx, ts_url in enumerate(ts_urls): task asyncio.create_task(download_and_decrypt_ts(session, ts_url, idx, key, cipher, base_path)) tasks.append(task) # 控制并发数避免请求过快被封 results await asyncio.gather(*tasks, return_exceptionsTrue) return [r for r in results if r and not isinstance(r, Exception)] # 运行异步任务 loop asyncio.get_event_loop() downloaded_files loop.run_until_complete(download_all_segments(playlist.segments, encryption_key, cipher))注意事项并发控制不要一次性发起成百上千个并发请求这极易触发服务器的速率限制或直接被封IP。可以使用asyncio.Semaphore来限制最大并发数例如设置为10-20。错误重试网络请求可能失败必须为每个下载任务添加重试机制如tenacity库。IV动态计算对于AES-128 CBC模式如果m3u8中没有指定IV且不是全零IV那么IV可能是每个切片媒体序列号#EXT-X-MEDIA-SEQUENCE 切片索引的某种转换如大端序的16字节。这需要仔细分析客户端JavaScript的解密逻辑是最深的水坑之一。3.4 第四步合并与转码所有.ts文件下载解密后它们是独立的。我们需要将其合并。# 方法一使用FFmpeg命令行最可靠 # 首先创建一个文件列表文件 filelist_path os.path.join(base_path, filelist.txt) with open(filelist_path, w, encodingutf-8) as f: for file in sorted(downloaded_files): # 确保按顺序 f.write(ffile {os.path.abspath(file)}\n) import subprocess output_mp4 final_video.mp4 # 使用FFmpeg的concat demuxer进行合并 cmd [ ffmpeg, -f, concat, -safe, 0, -i, filelist_path, -c, copy, # 直接流复制不重新编码速度极快 output_mp4 ] subprocess.run(cmd, checkTrue) print(f视频已合并至: {output_mp4}) # 方法二如果FFmpeg不可用可以纯Python二进制合并但可能无法处理编码问题 # with open(output_mp4, wb) as outfile: # for file in sorted(downloaded_files): # with open(file, rb) as infile: # outfile.write(infile.read())提示-c copy参数意味着“流复制”它只是将TS文件的音视频流拼接起来不进行耗时的重新编码因此速度非常快。但如果TS文件之间的编码参数有不一致极少见可能会导致播放问题此时可以去掉-c copy让FFmpeg重新编码-c:v libx264 -c:a aac但耗时将大大增加。4. 自动化完成“任务点”的终极挑战下载视频只是第一步我们的终极目标是让系统自动完成学习通上的“任务点”即标记视频已观看。这比下载要困难得多因为它涉及到与学习通前端逻辑的深度交互。4.1 任务点完成的机制分析通过抓包分析“正常播放完成”的过程你会发现心跳包视频播放期间网页或App会以固定间隔如每30秒或每分钟向一个特定API发送“心跳”请求上报当前播放位置、时长等信息。完成请求当视频播放到末尾或达到一定比例如95%会触发一个“任务点完成”的POST请求。参数依赖这些请求携带大量参数如clazzId班级ID、courseId课程ID、jobid任务ID、objectId对象ID即视频资源ID、playingTime播放时长、duration视频总长等。其中许多ID需要从课程页面源码或之前的API响应中提取。状态验证服务器会验证你的登录状态、任务是否开启、当前播放进度是否合理等。4.2 模拟完成请求的Python实现思路是先通过请求获取任务状态信息然后构造心跳和完成请求。# 假设我们已经从课程页面解析出了以下关键ID clazz_id 1234567 course_id 7654321 job_id 998877 object_id video_abc123 # 对应具体视频资源的ID user_id 888999 # 从Cookie或用户信息接口获取 # 1. 获取视频总时长可以从m3u8的#EXTINF累加或从另一个API获取 total_duration 1200 # 假设总时长1200秒 # 2. 模拟心跳请求每30秒发送一次 heartbeat_url https://mooc1.chaoxing.com/multimedia/log/a for current_time in range(0, total_duration 60, 30): # 多发送一次确保覆盖 heartbeat_data { clazzId: clazz_id, courseId: course_id, jobid: job_id, objectId: object_id, playingTime: current_time, duration: total_duration, otherInfo: , userid: user_id, isdrag: 0, view: pc, enc: 计算出的加密字符串, # 注意这个enc字段通常是关键需要逆向JS计算 } # 注意实际的enc字段计算非常复杂是学习通主要的反爬手段。 # 它可能由多个字段拼接后经过特定的MD5或AES加密生成。 # 必须通过分析player.js等前端文件找到加密函数并模拟。 resp requests.post(heartbeat_url, dataheartbeat_data, headersheaders, cookiescookie_dict) print(f心跳 {current_time}s: {resp.status_code}) time.sleep(0.5) # 适当延迟模拟真人 # 3. 模拟完成任务点请求 finish_url https://mooc1.chaoxing.com/job/multimedia/jobFinish finish_data { clazzId: clazz_id, courseId: course_id, jobid: job_id, objectId: object_id, playingTime: total_duration, # 播放到总时长 duration: total_duration, userid: user_id, isdrag: 3, # 完成状态码 view: pc, enc: 计算出的完成请求enc, # 同样需要计算 } finish_resp requests.post(finish_url, datafinish_data, headersheaders, cookiescookie_dict) if finish_resp.status_code 200 and finish_resp.json().get(isPassed): print(任务点已完成) else: print(任务点完成失败:, finish_resp.text)最大的难点——enc参数 这个字段是学习通反自动化脚本的核心。你需要使用浏览器开发者工具的“源代码”面板全局搜索enc、jobFinish、multimedia/log等关键词找到负责生成这个参数的JavaScript函数。通常是一个复杂的函数将多个参数按特定顺序拼接然后进行MD5哈希或者使用AES加密后再Base64编码。你需要使用Python的execjs库或PyExecJS来执行这段JavaScript代码或者更彻底地将它的逻辑用Python重写。4.3 应对滑块验证码等风控当你频繁发送请求或从非常用IP登录时可能会触发滑块验证码。识别请求返回特定状态码如403或响应内容中包含验证码相关关键词如verify、slide。应对策略降低频率在心跳请求间增加随机延迟模拟人类观看的不规律性。维持会话确保Cookie长期有效避免频繁重新登录。使用Selenium应对当检测到需要滑块时切换到Selenium自动化浏览器执行以下步骤from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time # 启动浏览器访问出现滑块的页面 driver webdriver.Chrome() driver.get(https://passport2.chaoxing.com/...) # 滑块验证页面URL # 定位滑块和缺口图片这需要分析页面结构 slide_block WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, slideBlock)) ) gap_bg driver.find_element(By.ID, gapBg) # 计算需要滑动的距离通过图片像素比对这是另一个复杂课题 # distance calculate_distance(gap_bg.screenshot_as_png, slide_block.screenshot_as_png) # 模拟人类滑动轨迹先加速后减速 # drag_and_drop(slide_block, distance) # 等待验证通过获取新的Cookie time.sleep(3) new_cookies driver.get_cookies() # 将new_cookies更新到requests的session中自动识别滑块缺口和模拟滑动轨迹是一个专门的领域可以使用图像识别库如opencv-python计算距离并用动作链模拟滑动。但这部分代码不稳定且平台会更新验证码样式。5. 常见问题与排查技巧实录在实战中你会遇到各种各样的问题。下面是一个快速排查指南问题现象可能原因排查步骤与解决方案获取m3u8返回403/4041. 请求头不完整或错误。2. Referer或Origin校验失败。3. Cookie过期或无效。4. URL带有时效性参数已过期。1.对比抓包用Fiddler抓取一次成功请求逐字段对比你的Python请求头特别是User-Agent,Referer,Accept-*。2.更新Cookie重新从浏览器获取Cookie或检查Cookie中关键会话字段如fid,UID是否存在。3.检查URLm3u8的URL可能包含t,token等参数需要确认其生成逻辑并正确获取。获取密钥返回403或空数据1. 密钥请求需要额外的认证头如Authorization: Bearer xxx。2. 密钥URI本身也受Referer保护。3. 密钥已过期动态密钥有效期很短。1.分析密钥请求在Fiddler中仔细查看成功的密钥请求看是否有自定义头部。2.模拟播放器行为尝试在获取m3u8后立即请求密钥不要有延迟。3.关联请求密钥的token可能来源于获取m3u8请求的响应头或响应体中的某个字段。下载的TS切片解密后无法播放1.密钥错误获取的密钥内容不对。2.IV错误AES CBC模式的初始化向量(IV)不正确。3.解密模式错误可能不是AES-128 CBC或者是CBC模式但填充方式不对。4.TS文件损坏下载不完整。1.验证密钥用抓包工具查看密钥请求的原始响应Hex视图确认是16字节二进制数据。2.测试IV尝试使用m3u8中指定的IV、全零IV、或用序列号计算的IV分别解密第一个切片用播放器或ffprobe测试哪个能播。3.检查填充尝试PKCS7、PKCS5或无填充。解密后末尾字节如果是规律的如0x01,0x02 0x02很可能就是填充。4.重新下载确保下载完整无网络错误。心跳或完成请求返回“参数错误”或“enc校验失败”1.enc字段计算错误。2. 请求参数缺失或格式不对。3. 服务器端风控。1.逆向JS这是最核心的一步。在浏览器Sources面板中搜索enc生成函数用Python复现其逻辑。重点检查参数拼接顺序、使用的哈希算法MD5, SHA1、是否进行了Base64或Hex编码。2.参数对比用Python脚本生成的数据包和浏览器发送的数据包进行逐字段对比。3.添加冗余参数有些参数看似无用但服务器会校验必须和浏览器发送的一模一样。触发滑块验证码1. 请求频率过高。2. IP地址异常。3. 行为模式不像真人。1.立即暂停停止当前脚本的所有请求。2.切换IP/账号如果可能更换网络环境或使用备用账号。3.引入随机延迟在后续脚本中在请求间加入time.sleep(random.uniform(1, 5))。4.升级为浏览器自动化对于必须完成的任务使用Selenium等工具手动或半自动地处理滑块。FFmpeg合并后音画不同步1. TS切片的时间戳不连续。2. 视频流和音频流的编码参数在中间发生变化。1.使用FFmpeg重新编码不要用-c copy改用-c:v libx264 -c:a aac让FFmpeg重新处理时间戳。2.检查源用ffprobe检查几个TS切片看它们的codec_type和time_base是否一致。不一致的源文件很难完美合并。最后的个人体会构建这样一个自动化系统其过程本身就是一个绝佳的学习项目它迫使你去深入理解HTTP协议、加密解密、前端逆向和反爬虫策略。然而必须清醒认识到这是一场“道高一尺魔高一丈”的博弈。平台的防御策略在不断升级今天的有效方法明天可能就会失效。因此你的代码需要具备良好的可维护性和适应性将易变的逻辑如URL构造、enc计算抽象成配置或独立的模块。更重要的是始终将技术的应用约束在合理的个人学习辅助范畴内尊重平台规则和知识产权。毕竟技术是工具如何使用它取决于你的初衷。