1. 网页视频链接为什么总是“藏起来”
很多人第一次做视频下载,都会卡在同一个地方:明明浏览器里视频播放得好好的,打开开发者工具却找不到一个能直接右键保存的.mp4。这不是你操作有问题,而是现在主流网课、直播回放、在线教育平台几乎都换成了流媒体分发方式。页面里播放的那段视频,往往不是一整个文件,而是被切成几百上千个小片段,通过播放列表动态拼接出来的。你看到的“一个视频”,在网络上其实是“一串请求”。
我拿一个真实场景举例。有位朋友给孩子买了网课,直播时没空看,回放又是限时的,过期就打不开了。他想把视频存到本地,试过录屏,一个多小时录下来画质掉得厉害,还特别占时间。后来他换了个思路:既然视频能在浏览器里播,那播放器一定向某个地址发起了请求,只要把这个地址找出来,就能直接下载原始文件。这个思路是对的,问题在于怎么高效地把地址找出来。
传统做法是打开浏览器开发者工具,切到 Network 面板,勾选 Media 过滤,然后刷新页面,在一堆请求里肉眼找.m3u8或.mp4。这个方法能用,但有几个明显的坑。第一,请求太多,媒体请求混在图片、脚本、接口里,筛选条件稍微不对就漏掉。第二,很多平台的视频地址带签名参数,过期时间很短,你手动复制下来可能已经失效。第三,遇到 HLS 流,你拿到的是.ts分片,单独下载一个没法播放,必须找到.m3u8播放列表再合并。第四,部分地址有 Referer 防盗链,直接下载返回 403。
所以真正省事的做法,是让程序自动完成“抓取请求 → 过滤媒体地址 → 提取播放列表 → 下载合并”这一整条链路。这也是本文要讲的核心:用 Claude Code 配合 Python,写一个能自动定位隐藏视频链接并下载的工具。它适合有基础 Python 环境、想批量保存授权范围内视频的读者,比如自己购买的课程、公司内部培训回放等。下面我会把可复制的配置、脚本和排障步骤都给出来,你跟着做就能跑通。
需要先说明一点:本文所有操作都建立在你有权访问并保存这些视频的前提下,比如自己购买的课程、自己录制的直播回放。请勿用于侵犯他人版权或绕过平台授权的场景。
2. TaoToken 前置:给 Claude Code 配好模型入口
Claude Code 本身是一个命令行里的编码助手,它能读你的项目文件、写代码、跑命令、根据报错自动修复。但它的“大脑”需要接一个大模型。默认情况下它连的是官方服务,国内访问不稳定,而且计费方式对个人开发者不太友好。更实际的做法是把它接到一个兼容 Anthropic 接口的模型服务上,TaoToken 就是这样一个入口,它提供 Claude Code 需要的 API 地址和密钥,你只需要改几个环境变量就能用。
先说清楚 TaoToken 在这里的角色:它不是抓包工具,也不替代你的编辑器,它只是给 Claude Code 提供模型推理能力。抓包和下载的逻辑,仍然是 Claude Code 帮你写出来的 Python 代码在跑。所以整个链路是:TaoToken 提供模型 → Claude Code 负责写代码和排障 → Python 脚本负责实际抓取和下载。
配置方式有两种,一种是临时环境变量,一种是写进配置文件。临时方式适合快速试一下,写进配置文件适合长期用。先看临时方式,在终端里执行:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="你的TaoToken密钥"Windows 的 PowerShell 里写法不同:
$env:ANTHROPIC_BASE_URL="https://taotoken.net/api" $env:ANTHROPIC_AUTH_TOKEN="你的TaoToken密钥"密钥在 TaoToken 控制台的 API Keys 页面创建,地址是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。创建后复制那一串以sk-开头的字符串,粘贴到上面的ANTHROPIC_AUTH_TOKEN位置。
如果你希望每次打开终端都自动生效,可以写进 shell 配置文件。macOS 和 Linux 用户编辑~/.zshrc或~/.bashrc,加上刚才那两行 export。Windows 用户可以在系统环境变量里新建这两个变量,或者用 Claude Code 自己的配置文件。
Claude Code 还支持一个settings.json配置文件,路径通常在用户目录下的.claude文件夹里。你可以直接写一个 JSON 片段,把模型入口固定下来:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "你的TaoToken密钥", "ANTHROPIC_MODEL": "claude-sonnet-4-5-20250929" } }这里三个字段要配齐:Base URL 指向 TaoToken 的 API 地址,Key 是你的密钥,Model ID 指定用哪个模型。Model ID 可以按需换成你账号里可用的其他模型。配好之后,在项目目录里运行claude,如果能看到欢迎界面并且底部显示模型名称,说明接入成功。
有一点要注意:Base URL 写https://taotoken.net/api就行,不要在后面加多余的路径。密钥不要提交到 Git 仓库,也不要在截图里暴露。如果你在团队里共用,建议每个人用自己的密钥,方便在控制台看用量。
配好模型入口之后,Claude Code 就能正常对话和写代码了。接下来我们进入正题:让它帮我们写一个抓包分析工具。你可以先在项目目录里建一个空文件夹,比如getpacket,然后cd进去运行claude,把需求描述清楚。描述需求时越具体越好,比如告诉它“我要一个 Python 工具,能列出系统进程、对选中进程抓包、提取视频链接”,它就会进入计划模式,先给你一份实现方案,你确认后再动手写。
3. 可复制配置:抓包过滤与下载脚本
这一节是全文的核心,我会把关键配置和脚本片段给出来。你不需要全部手敲,可以让 Claude Code 生成,但理解每一段在做什么,排障时才有方向。
先说抓包方案的选择。直接抓网卡原始数据包,只能看到加密后的 HTTPS 流量,拿不到里面的 URL。要解密 HTTPS,需要用中间人代理的方式:本地起一个代理服务,把系统流量导过去,代理用自签证书解密后再转发。Python 生态里mitmproxy就是干这个的,它支持写 addon 脚本,在请求经过时把 URL 和请求头打印出来。
抓包过滤的核心逻辑,是判断一个请求是不是视频。判断依据有几类:URL 后缀是不是.mp4、.m3u8、.ts、.flv、.mpd;响应头的Content-Type是不是video/*或application/vnd.apple.mpegurl;URL 里有没有video、stream、m3u8这类关键词。下面是一个 mitmproxy addon 的配置片段,保存为addons/video_extractor.py:
from mitmproxy import http import json import sys class VideoExtractor: def __init__(self): self.video_extensions = { '.mp4', '.m3u8', '.flv', '.ts', '.mkv', '.avi', '.f4v', '.webm', '.m4s', '.mpd' } self.video_keywords = ['video', 'stream', 'media', 'play', 'hls', 'mpeg'] def request(self, flow: http.HTTPFlow): url = flow.request.pretty_url url_lower = url.lower() # 优先识别 m3u8 播放列表 if '.m3u8' in url_lower: self._emit(flow, url, "m3u8_playlist") return # 识别视频文件后缀 if any(url_lower.endswith(ext) for ext in self.video_extensions): self._emit(flow, url, "video") return # 关键词 + 后缀双重判断,降低误报 if any(kw in url_lower for kw in self.video_keywords): if any(ext in url_lower for ext in ['.mp4', '.m3u8', '.flv', '.ts', '.mpd']): self._emit(flow, url, "video") def response(self, flow: http.HTTPFlow): if not flow.response: return ct = flow.response.headers.get('content-type', '').lower() if 'mpegurl' in ct or 'm3u8' in ct: self._emit(flow, flow.request.pretty_url, "m3u8_playlist") elif 'video' in ct or 'dash' in ct: self._emit(flow, flow.request.pretty_url, "video") def _emit(self, flow, url, kind): headers = {} for key in ['Referer', 'User-Agent', 'Origin', 'Authorization', 'Cookie']: value = flow.request.headers.get(key, '') if value: headers[key] = value info = {"url": url, "type": kind, "headers": headers} print(f"[VIDEO] {json.dumps(info, ensure_ascii=False)}") sys.stdout.flush() addons = [VideoExtractor()]这段脚本的关键点在于:它把 URL 和请求头一起输出成 JSON,尤其是Referer。很多平台用 Referer 做防盗链,你下载时如果不带这个头,服务器直接返回 403。所以抓取阶段就要把 Referer 存下来,下载阶段原样带上。
启动 mitmproxy 并加载这个 addon,命令是:
mitmdump --listen-host 127.0.0.1 --listen-port 8080 -s addons/video_extractor.py启动后,把系统代理指向127.0.0.1:8080。Windows 可以在“设置 → 网络和 Internet → 代理”里手动填,也可以用脚本改注册表。macOS 在“系统设置 → 网络 → 代理”里配置。配置完代理后,第一次访问 HTTPS 网站会提示证书不受信任,需要把 mitmproxy 的 CA 证书装到系统信任区。证书文件在用户目录的.mitmproxy文件夹里,文件名是mitmproxy-ca-cert.pem。Windows 用管理员权限执行:
certutil -addstore -f ROOT "%USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.pem"装完证书后重启浏览器,再播放视频,终端里就会刷出[VIDEO]开头的行。
拿到 m3u8 地址和 Referer 之后,就是下载环节。HLS 视频的下载逻辑是:先请求 m3u8 文件,解析出所有.ts分片地址;如果 m3u8 里有#EXT-X-KEY行,说明分片是 AES-128 加密的,要先下载密钥;然后逐个下载分片、解密、按顺序拼接成一个文件。下面是一个精简版的下载脚本hls_download.py:
import argparse import os import re import struct import tempfile import urllib.parse from typing import List, Optional, Tuple import requests try: from Crypto.Cipher import AES HAS_CRYPTO = True except ImportError: HAS_CRYPTO = False class HLSDownloader: def __init__(self, referer: str = "", user_agent: str = ""): self.referer = referer self.user_agent = user_agent or ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" ) self.session = requests.Session() self.session.headers.update({"User-Agent": self.user_agent}) if referer: self.session.headers["Referer"] = referer def fetch(self, url: str, binary: bool = False): resp = self.session.get(url, timeout=30) resp.raise_for_status() return resp.content if binary else resp.text def parse_m3u8(self, content: str, base_url: str) -> Tuple[List[str], Optional[dict]]: ts_urls = [] key_info = None base_dir = base_url.rsplit('/', 1)[0] for line in content.splitlines(): line = line.strip() if line.startswith('#EXT-X-KEY:'): key_info = self._parse_key(line, base_url) elif line and not line.startswith('#'): if not line.startswith('http'): if line.startswith('/'): p = urllib.parse.urlparse(base_url) line = f"{p.scheme}://{p.netloc}{line}" else: line = f"{base_dir}/{line}" ts_urls.append(line) return ts_urls, key_info def _parse_key(self, line: str, base_url: str) -> Optional[dict]: info = {} m = re.search(r'METHOD=([^,]+)', line) if m: info['method'] = m.group(1) m = re.search(r'URI="([^"]+)"', line) if m: uri = m.group(1) if not uri.startswith('http'): base_dir = base_url.rsplit('/', 1)[0] uri = f"{base_dir}/{uri}" info['uri'] = uri m = re.search(r'IV=0x([0-9a-fA-F]+)', line) if m: info['iv'] = bytes.fromhex(m.group(1)) return info or None def download(self, m3u8_url: str, output: str) -> bool: content = self.fetch(m3u8_url) # 处理嵌套 m3u8 if '.m3u8' in content and '#EXT' in content: for line in content.splitlines(): line = line.strip() if '.m3u8' in line and not line.startswith('#'): if not line.startswith('http'): base_dir = m3u8_url.rsplit('/', 1)[0] line = f"{base_dir}/{line}" return self.download(line, output) ts_urls, key_info = self.parse_m3u8(content, m3u8_url) if not ts_urls: print("未解析到分片地址") return False print(f"共 {len(ts_urls)} 个分片") key = None if key_info and key_info.get('method') == 'AES-128' and key_info.get('uri'): if not HAS_CRYPTO: print("需要安装 pycryptodome: pip install pycryptodome") return False key = self.fetch(key_info['uri'], binary=True)[:16] print("检测到 AES-128 加密,已获取密钥") tmp_dir = tempfile.mkdtemp(prefix='hls_') files = [] for i, url in enumerate(ts_urls): data = self.fetch(url, binary=True) if key: iv = key_info.get('iv') or (struct.pack('>I', i) + b'\x00' * 12) cipher = AES.new(key, AES.MODE_CBC, iv) data = cipher.decrypt(data) path = os.path.join(tmp_dir, f"seg_{i:05d}.ts") with open(path, 'wb') as f: f.write(data) files.append(path) print(f"\r下载进度 {i + 1}/{len(ts_urls)}", end='') print() with open(output, 'wb') as out: for path in files: with open(path, 'rb') as f: out.write(f.read()) size = os.path.getsize(output) / 1024 / 1024 print(f"完成: {output} ({size:.2f} MB)") return True def main(): parser = argparse.ArgumentParser(description='HLS 视频下载工具') parser.add_argument('url', help='m3u8 或 mp4 地址') parser.add_argument('-r', '--referer', default='', help='Referer 头') parser.add_argument('-u', '--user-agent', default='', help='User-Agent 头') parser.add_argument('-o', '--output', default='video.mp4', help='输出文件名') args = parser.parse_args() dl = HLSDownloader(args.referer, args.user_agent) if args.url.endswith('.mp4'): data = dl.fetch(args.url, binary=True) with open(args.output, 'wb') as f: f.write(data) print(f"完成: {args.output}") else: dl.download(args.url, args.output) if __name__ == '__main__': main()依赖装两个就够:
pip install requests pycryptodome用法很直接,把抓到的 m3u8 地址和 Referer 填进去:
python hls_download.py "https://example.com/video/index.m3u8" -r "https://example.com/" -o lesson01.mp4如果是直接的 mp4 地址,脚本会走另一条分支,直接下载。这里有个细节:AES-128 的 IV 如果 m3u8 里没写,默认用分片序号构造,脚本里已经处理了。分片下载顺序必须严格按 m3u8 里的顺序,否则拼出来的视频会花屏或音画不同步。
4. 验证请求:从抓包到文件落地的完整结果
配置写完之后,最重要的是验证整条链路真的通了。我按实际操作顺序拆成几步,每一步都有明确的成功标志,你可以对照检查。
第一步,启动抓包服务。在项目目录运行mitmdump命令,看到类似Proxy server listening at http://127.0.0.1:8080的输出,说明代理起来了。这时候系统代理还没配,浏览器流量不会经过它。配好系统代理后,随便打开一个 HTTPS 网站,终端里应该开始刷请求日志。如果一条都没有,说明代理没生效,回去检查系统代理设置。
第二步,播放目标视频。在浏览器里打开你要保存的课程或回放,点播放。终端里会刷出[VIDEO]开头的 JSON 行。你要找的是"type": "m3u8_playlist"的那条,它的url字段就是播放列表地址,headers里的Referer就是下载时要带的头。如果只看到一堆.ts分片地址,没看到 m3u8,说明你是在视频播放中途才开始抓的,播放列表请求已经过去了。解决办法是停止抓包、清空日志,然后从头刷新页面重新播放,让播放列表请求重新出现。
第三步,复制地址和 Referer。把 m3u8 的 URL 和 Referer 值分别复制出来。注意 Referer 通常只到域名,比如https://live-web.example.com/,不要多复制路径。有些平台的 Referer 带完整路径,那就原样复制。
第四步,运行下载脚本。把地址和 Referer 填进命令:
python hls_download.py "https://stream.example.com/xxx/index.m3u8?id=abc" -r "https://live-web.example.com/" -o lesson01.mp4成功的话,终端会先打印分片总数,然后进度条一路走到 100%,最后输出文件大小。我实测一个 189 个分片的课程,下载加解密加合并大概两分钟,输出文件 185 MB 左右。用播放器打开,画面和声音都正常,拖动进度条也没问题。
第五步,校验文件完整性。下载完成后,可以用ffprobe看一下时长和编码信息:
ffprobe -v error -show_entries format=duration,size -of default=noprint_wrappers=1 lesson01.mp4如果输出的时长和课程实际时长对得上,说明分片没漏。如果时长明显偏短,可能是某些分片下载失败被跳过了,需要重新下载。脚本里对失败分片是跳过处理的,生产环境可以改成重试三次。
这里再补充一个常见情况:有些平台的 m3u8 是嵌套的,主播放列表里只有一行子 m3u8 地址,真正的分片列表在子文件里。脚本里已经做了递归处理,遇到嵌套会自动往下解析。如果你手动看 m3u8 内容,发现里面只有#EXT-X-STREAM-INF加一行地址,那就是嵌套结构,交给脚本处理就行。
验证通过之后,你就有了一个可复用的流程:抓包拿地址 → 填参数下载 → 校验文件。下次遇到同类视频,重复这几步即可。如果视频很多,可以把地址和 Referer 写进一个列表,循环调用下载函数,实现批量保存。
5. 本篇常见错排查:403、证书、分片失败
实际操作中,报错是难免的。我把几个高频问题和对应的排查思路整理出来,你遇到时可以直接对照。
403 Forbidden,提示 denied by Referer ACL。这是最常见的错误,原因是下载请求没带 Referer,或者 Referer 值不对。服务器通过 Referer 判断请求来源,来源不合法就拒绝。解决办法是把抓包时记录的 Referer 原样带上。如果你用的是requests,检查session.headers里有没有设置Referer。如果带了还是 403,可能是 Referer 需要精确到某个路径,或者还需要Origin头。把抓包输出里的Origin也一起带上试试。还有一种情况是地址带签名参数且已过期,重新抓一次拿新地址。
证书报错,浏览器提示连接不安全。这是 mitmproxy 的 CA 证书没装到系统信任区。表现是配了代理之后,所有 HTTPS 网站都打不开,或者视频加载失败。解决办法是按前面说的,用certutil命令把mitmproxy-ca-cert.pem装到“受信任的根证书颁发机构”。装完之后,浏览器和部分应用需要完全重启才能识别新证书。注意是重启进程,不是关标签页。如果重启后还不行,检查证书是不是装到了“个人”而不是“受信任的根证书颁发机构”。
ImportError: cannot import name 'controller' from 'mitmproxy'。这是 mitmproxy 版本升级导致的 API 变化,旧代码里from mitmproxy import controller在新版本里已经移除。解决办法是不要直接调用 mitmproxy 的 Python API,改用命令行方式启动mitmdump,通过-s加载 addon 脚本。这样版本兼容性最好,也更容易调试。
分片下载失败,输出文件时长偏短。原因可能是某个分片请求超时,或者被限流。脚本里对失败分片是跳过的,所以文件能生成但不完整。排查方法是看下载日志里有没有跳过的分片序号,然后单独重试那个分片。更稳妥的做法是给fetch加重试逻辑,失败后等一秒再试,最多三次。另外,分片下载不要太快,有些服务器对高频请求会限流,可以在每个分片之间加一个短 sleep。
下载下来的文件无法播放,或者只有声音没画面。这通常是解密环节出了问题。检查 m3u8 里有没有#EXT-X-KEY行,如果有,确认密钥下载成功、IV 计算正确。AES-128 的 IV 如果 m3u8 里指定了IV=0x...,必须用指定的值;如果没指定,才用分片序号构造。用错了 IV,解密出来的数据是乱的,播放器识别不了。还有一种可能是分片顺序错了,检查拼接时是不是严格按 m3u8 里的顺序。
抓包抓不到任何视频请求。先确认系统代理是否真的生效,可以在浏览器里访问一个显示 IP 的网站,看出口 IP 有没有变。如果没变,说明代理没配上。其次确认目标应用是不是走了系统代理,有些播放器用自己的网络栈,不读系统代理设置。这种情况需要在应用内单独配置代理,或者用透明代理模式。最后确认证书装好了,证书没装的话 HTTPS 请求会直接失败,日志里也不会有内容。
进程列表里一堆 msedgewebview2,分不清哪个是浏览器。WebView2 是很多桌面应用内嵌的浏览器组件,不只是 Edge 在用。区分方法是看进程的窗口标题和父进程。浏览器主进程通常有明确的窗口标题,WebView2 组件的标题可能是空的或者显示宿主应用名。如果只是想抓浏览器里的视频,直接在浏览器里操作,不用纠结选哪个进程,因为系统代理抓的是全局流量,跟选哪个进程无关。
6. 语义一致 CTA:把工具用起来
到这里,抓包、解析、下载、排障的完整链路就讲完了。你可以按这个顺序动手:先用 TaoToken 把 Claude Code 的模型入口配好,让它帮你生成和调试脚本;然后启动 mitmproxy 抓包,定位 m3u8 地址和 Referer;最后用下载脚本把视频保存到本地。整个过程里,Claude Code 负责写代码和修 bug,你负责确认需求和验证结果。
如果你在配置模型入口时遇到问题,比如密钥无效、模型名不对,可以去 TaoToken 的接入文档看详细说明:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。文档里有各种客户端的配置示例,包括 Claude Code、Cline、Codex 这些。想先试试模型对话效果,可以打开 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 直接体验。如果你打算长期用 Claude Code 做编码和 Agent 任务,Coding Plan 会更划算,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。
最后提醒一句:抓包和下载技术本身是中性的,用在自己有权访问的内容上是合理的,比如自己买的课、自己录的回放。用之前确认一下平台的授权范围,别把工具用在侵犯版权的地方。工具是死的,怎么用取决于人。