流媒体试看时长破解
现在的很多视频网站都采用流媒体分片技术进行分发视频,目前主流的是HLS技术
在一些视频网站中会限制视频时长,只能播放前15s
分析m3u8文件结构可以发现类似下面这种有规律的信息结构,而且不存在资源校验。
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:11
#EXTINF:10.000,
url_590/1_mp4_h264_aac_fhd_7.ts
#EXTINF:10.000,
url_591/1_mp4_h264_aac_fhd_7.ts
#EXTINF:10.000,
....
那么就可以通过二分查找总视频分片长度,从而得到完整的m3u8视频文件,配合N_m3u8DL-RE.exe 下载到本地。
import re
import requests
from requests.adapters import HTTPAdapter
from urllib.parse import urlparse, urljoin
class HttpClient:
def __init__(self, retries=3, timeout=5):
self.adapter = HTTPAdapter(max_retries=retries)
self.session = requests.Session()
self.session.mount("http://", self.adapter)
self.session.mount("https://", self.adapter)
self.timeout = timeout
def head(self, url):
"""发送 HEAD 请求检测资源是否存在"""
try:
resp = self.session.head(url, timeout=self.timeout)
return resp.status_code
except requests.RequestException:
return 404
def get_text(self, url):
"""获取文本内容 (M3U8)"""
try:
resp = self.session.get(url, timeout=self.timeout)
resp.raise_for_status()
return resp.text
except requests.RequestException as e:
print(f"Error fetching URL: {e}")
return None
class SegmentFinder:
def __init__(self, http_client: HttpClient, base_url_pattern: str):
self.http = http_client
self.url_pattern = base_url_pattern # 格式如: https://.../segment_{}.ts
def _check_exists(self, index):
"""检查特定索引的 TS 切片是否存在"""
url = self.url_pattern.format(index)
status = self.http.head(url)
# print(f"Checking {index} -> {status}") # 调试用
return status == 200
def find_last_segment_index(self, start_guess=3000):
"""
组合策略:
1. 指数搜索 (Exponential Search) 确定上界
2. 二分搜索 (Binary Search) 确定精确值
"""
low = 0
high = start_guess
print(">>> 正在探测最大边界...")
# 1. 指数扩容寻找上界 (如果3000存在,就查6000,以此类推)
while self._check_exists(high):
print(f"索引 {high} 存在,扩大搜索范围...")
low = high
high *= 2
if high > 100000: # 防止死循环的安全熔断
print("警告:切片数量异常巨大,强制停止扩容")
break
print(f">>> 锁定范围: {low} - {high},开始二分查找...")
# 2. 二分查找精确值
final_index = low
while low <= high:
mid = (low + high) // 2
if self._check_exists(mid):
final_index = mid
low = mid + 1
else:
high = mid - 1
return final_index
class M3U8Restorer:
def __init__(self, original_url):
self.original_url = original_url
self.http = HttpClient()
self.base_url = original_url.rsplit('/', 1)[0] + '/'
self.content = ""
self.headers = []
self.ts_pattern = "" # URL 格式化字符串
self.ts_duration = "" # 单片时长
self.ts_prefix = "" # 文件名前缀
self.ts_suffix = "" # 文件名后缀
self.start_index = 0 # 起始索引
def _parse_original(self):
"""解析原始 M3U8,提取头部信息、加密Key、文件名模式"""
self.content = self.http.get_text(self.original_url)
if not self.content:
raise Exception("无法下载原始 M3U8")
lines = self.content.strip().split('\n')
self.headers = []
ts_examples = []
for line in lines:
line = line.strip()
if line.startswith('#EXTINF'):
if not self.ts_duration:
self.ts_duration = line # 暂且使用第一个切片的时长作为默认
elif line.startswith('#') and not line.startswith('#EXT-X-ENDLIST'):
self.headers.append(line)
elif line and not line.startswith('#'):
ts_examples.append(line)
# 分析文件名模式 (假设类似 123_i0.ts)
if ts_examples:
last_ts = ts_examples[-1] # 取最后一个做分析
# 正则提取: (前缀)(数字)(后缀)
# 例如: "data005.ts" -> "data", "005", ".ts"
match = re.search(r'^(.*?)(\d+)(\.[a-zA-Z0-9]+)$', last_ts)
if match:
self.ts_prefix = match.group(1)
num_str = match.group(2)
self.ts_suffix = match.group(3)
# 记录原始的数字长度(如果是001, 002这种填充格式)
# 这里为了简化,我们假设是用 format 生成
self.ts_pattern = f"{self.base_url}{self.ts_prefix}{{}}{self.ts_suffix}"
self.start_index = int(num_str) # 预览版最后一个切片的索引
else:
# 简单回退策略
self.ts_pattern = urljoin(self.base_url, last_ts.replace(str(self.start_index), "{}"))
def run(self):
print(f"正在分析: {self.original_url}")
self._parse_original()
print(">>> 开始探测完整分片数量...")
finder = SegmentFinder(self.http, self.ts_pattern)
# 从预览版最后一个索引开始往后找
total_last_index = finder.find_last_segment_index(start_guess=3000)
print(f">>> 探测结束,最大索引为: {total_last_index}")
return self._build_full_m3u8(total_last_index)
def _build_full_m3u8(self, last_index):
"""重组完整的 M3U8 字符串"""
lines = []
# 1. 写入头部 (Version, TargetDuration, Key 等)
for h in self.headers:
lines.append(h)
# 2. 循环生成切片列表
# 注意:这里假设从 0 开始。如果原始视频不是从 0 开始,需要调整 range
for i in range(0, last_index + 1):
lines.append(self.ts_duration) # 使用提取的时长
# 构造完整 URL (有些播放器需要完整 URL,有些只需要相对路径)
# 这里为了保险,直接放完整 URL
full_ts_url = self.ts_pattern.format(i)
lines.append(full_ts_url)
# 3. 写入结尾
lines.append("#EXT-X-ENDLIST")
return "\n".join(lines)
if __name__ == "__main__":
target_url = "https://xxxxx/a9f51d22edef4618a8600a0e263b2771_preview.m3u8"
try:
restorer = M3U8Restorer(target_url)
full_m3u8_content = restorer.run()
# 输出结果(可以写入文件)
print("\n" + "=" * 20 + " 恢复结果 " + "=" * 20)
# 仅打印前10行和后10行
preview_lines = full_m3u8_content.split('\n')
print("\n".join(preview_lines[:10]))
print("...")
print("\n".join(preview_lines[-5:]))
# 保存到文件
with open("full_movie.m3u8", "w", encoding="utf-8") as f:
f.write(full_m3u8_content)
print(f"\n已保存完整文件到 full_movie.m3u8,共 {len(preview_lines) // 2} 个切片")
except Exception as e:
print(f"处理出错: {e}")

浙公网安备 33010602011771号