☰
Claude Code 实战:自动捕获隐藏视频链接并下载到本地
2026/10/15 4:45:39 网站建设 项目流程

1. 为什么要把“隐藏的视频链接”当成一个正经项目来做

做网页开发或者内容运营的人,应该都撞上过这类尴尬场景:页面上视频播放得好好的,鼠标右键一看,压根没有“另存为视频”的选项;打开浏览器开发者工具往下翻,HTML 源码里要么空空荡荡,要么只有一个<div id="app">,视频地址不知道藏到哪个变量的某个深层字段里。我最早遇到这个问题,是在做一个公司内部培训系统的前端联调任务,需要确认页面调用了哪个 CDN 的哪个视频文件,结果找了半天找不着。从那天起我就意识到,所谓“隐藏的视频链接”,其实是现代 Web 页面普遍存在的动态加载机制留下的现象,并不神秘,但要做成一套能自动“发现链接、验证可用性、下载到本地”的工具,就得掌握一套完整的技术路径。

这个项目标题里带上了 Claude Code,也正是因为这类工作的核心不是去网上搜一段单页脚本,而是需要反复分析页面结构、调整捕获策略、验证下载结果。说白了,这是一个典型的“分析、编码、调试、迭代”闭环任务,非常适合让 AI 编码助手帮我打下手。Claude Code 能直接读取项目文件、给出修改方案、生成新版脚本,我来判断适配和边界,一个人完成一个原本可能要写两三天的工具箱,最后还能沉淀成可复用的内部脚本。这就是我决定动手做这次实战的原因。

这篇博文适合这么几类人看:平时做爬虫和页面自动化的人,想搞明白动态渲染页面里媒体地址怎么定位;前端开发人员,想给自家页面排查视频加载路径;以及各种想用 AI 工具提升开发效率、但还没找到合适案例的读者。接下来我会按真实的项目推进过程来写,包括需求拆解、工具准备、技术原理、代码实现、实测记录和踩坑清单,全部是实际操作中会碰到的细节。

1.1 需求拆解:标题背后到底包含几层信息

只看标题,很容易误以为这只是一个“找地址、下载”的简单脚本。真正拆开看,其实有至少四层任务。

第一层是“捕获”。现代页面里的视频地址,通常不是一个人能肉眼从 HTML 源码里直接读出来的,它可能要等播放器初始化、等用户点击播放按钮、等某个接口返回凭证之后,才会出现在内存里或者某个请求里。所谓捕获,就是要在正确的时间点,用正确的方式记录下这个地址。

第二层是“识别”。拿到的地址五花八门,有的以.mp4结尾,有的带着一长串签名参数,有的指向一个.m3u8播放列表文件,还有一些其实是 CDN 的临时跳转地址。脚本需要从一堆混合内容里把真正的媒体资源筛选出来。

第三层是“验证”。捕获到的地址不代表就能直接下载。它可能有时效性,可能在几十秒后过期,可能对来源站点有校验。需要控制变量去验证,是本地网络问题、还是地址本身失效、还是缺少必要的请求头。

第四层才是“下载”。下载也不是一个 get 请求把文件拿回来那么简单。面对几个 GB 的大文件,需要断点续传和超时处理;面对各种防盗链机制,需要补上 Referer、User-Agent 甚至 Token;面对流式切片的视频,则需要理解分段协议。

理解了这四层之后你会发现,写代码反而不是最大的难点,真正的难点在于怎么分析页面行为、怎么设计捕获方案。恰恰是这部分,Claude Code 在对话过程中能给我很多启发式的建议,而不是纯粹靠搜索引擎搜答案。

1.2 技术路线:从页面分析到脚本落地的完整闭环

我给这个项目定的技术路线是三条腿走路:静态分析、动态捕获、辅助下载。

静态分析,指的是直接请求页面 HTML,用解析库找出<video>、<source>标签,以及页面里可能内嵌的数据配置。这条路适合网站结构简单、服务端渲染的场景,但不是主流,因为现在视频网站大多用前端框架搭起来,静态源码里只有一堆骨架标签。

动态捕获是核心,用无头浏览器加载页面,监听所有网络请求,把符合媒体特征地址的请求记录下来。这种方式可以抓到几乎所有在浏览器里真实发生的媒体请求,不会漏掉动态拼接出来的地址,也不会漏掉经过跳转的 CDN 地址,这也是“隐藏”一词真正对应的技术场景。

辅助下载则是拿到地址之后的处理层。它要考虑下载头、超时、重试、分片存储等问题。我中间还让 Claude Code 帮我做过一版小文件批量下载器,用来处理一个产品官网上一大堆几十 MB 的演示素材,速度比边看边点另存为快得多。

整个项目当时的核心决策,就是承认“页面行为是黑盒”,不做任何页面结构上的假设,而是从网络通信层面去捕获记录。这么做的好处是通用,任何一个浏览器能播放的视频页面,都能用这套办法找到媒体地址;代价则是代码比单纯读标签复杂一些,工具链也更重。但从实用角度看,这套方案绝对是最稳妥的。

2. 环境准备:把 AI 编码助手和浏览器自动化工具配齐

动手写脚本之前,先把环境搭好。这个项目用到的工具不少,但都不复杂:Claude Code 作为主要的编码协作工具,浏览器自动化用 Playwright,静态解析用 Python 的 BeautifulSoup,下载用 requests。全程只需要一个系统终端,加上一个本地的 Python 环境。

2.1 Claude Code 的基本使用方式

Claude Code 本身是一个终端里的 AI 编码工具,它可以在项目目录下直接运行,读取当前目录的文件,也能按要求生成或修改代码。对我来说,它最大的价值不是帮我“写”脚本,而是陪我把这个项目一步步想清楚。

实际操作中它的对话入口很直接,在项目目录里打开终端,运行启动命令进入交互界面,然后在提示符里用自然语言描述需求。比如说“分析一下当前目录,告诉我这里缺什么”或者“根据这个需求生成一个 Python 脚本”,它就会给出结果和解释。因为 Claude Code 能读取文件内容,所以它给出的代码往往更贴合实际项目状态,而不是一个脱离上下文的示例片段。

这里先提醒一句:AI 编码助手给出的代码,不等于最终可以直接跑。尤其是涉及网络请求、页面自动化这类对执行环境敏感的代码,很可能在你的机器上跑不通。我在项目开始前就把这个预期放平了,Claude Code 出方案,我负责验证和修偏差,合作关系反而舒服。

2.2 项目目录、Python 环境和依赖选型

我习惯把所有代码放在一个单独目录里,命名直接用英文。这个项目目录大致规划为这样:

video-capture/ ├── requirements.txt ├── scripts/ │ ├── static_finder.py │ ├── dynamic_capture.py │ ├── downloader.py │ └── utils.py └── output/

scripts放核心脚本,output放下载下来的文件。utils.py放公共的请求头、日志处理和去重函数。这样分区的好处是,后面每次针对不同网站分析时,只需要新增一个分析脚本,不用动公共逻辑。

依赖我选了三个:

  • beautifulsoup4和lxml,负责解析静态 HTML。
  • playwright,负责无头浏览器动态捕获。
  • requests,负责最终下载。

安装指令就几行:

mkdir video-capture cd video-capture python -m venv venv source venv/bin/activate pip install beautifulsoup4 lxml playwright requests playwright install chromium

最后一步playwright install chromium特别重要,它会下载一个渲染引擎。如果这一步没装,后面动态捕获脚本运行时,打开浏览器会直接报错,因为基础浏览器内核不存在。我第一次搭环境的时候就漏执行了这条命令,卡了好几分钟才反应过来。

3. 原理剖析:视频链接是怎么在页面里“藏”起来的

很多没接触过前端工程化的读者,可能会觉得“隐藏”这个词有某种神秘色彩。其实放到浏览器运行机制里看,逻辑相当清晰:页面原本没有视频地址,而是通过 JavaScript 从接口、配置对象或者实时协议里,把地址拿回来再填进播放器。这就是动态渲染。

3.1 静态源码、动态渲染和网络请求的三层关系

经典的网页结构里,服务器返回的 HTML 文档已经包含所有内容,右键查看源码就能看到视频标签。但现代 Web 应用大量使用前端框架,页面结构是 JavaScript 在浏览器里执行的产物。你看到的播放器框,初始状态下可能只是一堆空标签,真正的<video>元素和src属性,要等到一段代码在客户端运行后才被动态创建出来。

媒体地址藏身的“位置”,大概有这么几类:

位置说明常见特征
服务端渲染 HTML页面在服务器端输出时就直接带上完整标签<video src="...">
页面内嵌脚本数据数据被写进<script>标签的某个变量里window.__INITIAL_STATE__
接口返回播放器启动后请求一个接口拿到地址JSON 里的video_url字段
运行时拼接地址由多个常量拼接而来,依赖时间戳或 tokenbase + "/segment/" + id

对于前两种,静态分析就能搞定;后两种,必须等代码真正执行才能看清地址。这个区分决定了脚本的设计方向。

3.2 三条捕获路径:源码、事件、内存快照

对应上面的藏身逻辑,我有三条捕获路径可以走。

第一条是“源码路径”。直接用 requests 拿到 HTML 文本,再用 BeautifulSoup 查找标签。它的适用场景很有限,只对服务端渲染页面有效,但实现成本最低,通常作为脚本的第一道筛选。

第二条是“网络事件路径”,也是项目里最常用的路径。用 Playwright 打开页面后,浏览器的每个网络请求都会触发事件,我只需要监听这个事件,判断请求 URL 是否带有视频媒体的特征,比如.mp4、.m3u8、video、playlist等关键词,就能把地址记录下来。不管页面怎么动态拼接,只要浏览器真的发出了这个请求,事件层就能捕获到。相当于切开了黑盒子,直接看内部数据流。

第三条是“内存快照路径”。有些播放器会把视频地址放在全局变量或者 DOM 属性里,不上网请求也能拿到。通过 Playwright 的page.evaluate()方法,可以像在浏览器控制台里敲代码一样,直接读取window上的对象。这条路径实践中用得不多,但偶尔能帮上大忙。

对我而言,这三条路径不是三选一,而是按顺序执行。先静态找一遍,找不到再用动态捕获;如果捕获到的地址无法下载,再回到页面里读取内存快照对比。层级分明,排查问题的时候效率高很多。

4. 核心实现:从自然语言需求到可运行的下载脚本

环境搭好、原理也理清楚之后,就进入了项目最核心的部分:用 Claude Code 把方案变成代码,然后反复迭代。这一节我会把过程中的关键对话思路、核心代码片段和背后逻辑都写清楚。

4.1 第一步:用自然语言让 Claude Code 生成静态查找脚本

我在 Claude Code 里给的第一条指令,内容大致是这样:

帮我写一个 Python 脚本,名为 static_finder.py。它接收一个 URL 参数,请求页面,然后用 BeautifulSoup 解析所有 video 和 source 标签,提取出所有可能是视频资源的地址并去重输出。请求的时候要带上常见的浏览器 User-Agent。

Claude Code 生成的第一版代码基本可用,核心逻辑如下:

import sys import requests from bs4 import BeautifulSoup HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/124.0 Safari/537.36" } def find_video_srcs(url: str) -> list[str]: resp = requests.get(url, headers=HEADERS, timeout=15) resp.raise_for_status() soup = BeautifulSoup(resp.text, "html.parser") srcs = [] for video in soup.find_all("video"): if video.get("src"): srcs.append(video.get("src")) for source in video.find_all("source"): if source.get("src"): srcs.append(source.get("src")) # 页面里常见的视频字段名也顺带找一遍 for script in soup.find_all("script"): text = script.string or "" for key in ("videoUrl", "video_url", "playUrl"): if key in text: # 用简单方式截取附近内容中的 URL idx = text.find(key) chunk = text[idx: idx + 500] if "http" in chunk: start = chunk.find("http") end = chunk.find('"', start) if end != -1: srcs.append(chunk[start:end]) return list(dict.fromkeys(srcs)) if __name__ == "__main__": target = sys.argv[1] for url in find_video_srcs(target): print(url)

这个脚本里有一个关键设计:不仅找<video>标签,还会顺藤摸瓜看<script>标签里有没有视频字段。因为很多页面不会在 HTML 里放视频标签,但会把数据藏在videoUrl这类变量里。我把这种情况留言给 Claude Code 后,它自动加了这一段。实际测试里,这段确实能从一些“看似没有视频”的页面里抓到地址。

不过静态方式能覆盖的页面有限,大多数情况下第一版脚本跑完,输出列表是空的。这种结果也正常——真正需要花费精力处理的,是下面的动态捕获。

4.2 第二步:让 Claude Code 生成动态网络捕获脚本

动态捕获是这个项目的核心模块。需求描述要尽量具体,我给 Claude Code 的指令是这样的:

写一个 Playwright 脚本,用 Chromium 无头浏览器打开网页,监听页面上所有的网络请求,记录 URL 中包含 .mp4、.m3u8、.ts 或 video 的请求。等页面加载完成后,额外等待 8 秒,确保可能延迟触发的视频请求也发出。最后把抓到的地址打印出来。

Claude Code 给的代码如下:

import asyncio from playwright.async_api import async_playwright MEDIA_KEYWORDS = (".mp4", ".m3u8", ".ts", "video", "playlist", "media") async def capture_video_urls(page_url: str, wait_seconds: int = 8) -> list[str]: found_urls = [] async def on_request(request): raw_url = request.url.lower() if any(keyword in raw_url for keyword in MEDIA_KEYWORDS): found_urls.append(request.url) async with async_playwright() as p: browser = await p.chromium.launch(headless=True) page = await browser.new_page( user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/124.0 Safari/537.36" ) page.on("request", on_request) await page.goto(page_url, wait_until="networkidle", timeout=60000) await page.wait_for_timeout(wait_seconds * 1000) await browser.close() return list(dict.fromkeys(found_urls)) if __name__ == "__main__": target = "https://example.com/demo" urls = asyncio.run(capture_video_urls(target)) for idx, u in enumerate(urls, 1): print(f"[{idx}] {u}")

这段代码的巧妙之处,在于它不对页面结构做任何预设,只是从网络事件入手。哪怕页面 10 层嵌套、代码再混淆,只要最后浏览器发出了媒体请求,就能被捕获。

但真正跑到这一步,问题也来了:打印出来的内容可能很杂。有些地址看起来像接口地址而不是视频本身,有些是广告资源的路径,有些是封面图。这时候就要用第二轮对话来优化。

4.3 第三步:迭代优化,让捕获结果更干净

我向 Claude Code 反馈了一堆问题:“捕获到太多无关请求”“部分地址带签名参数不知道能不能直接用”“能否帮我按扩展名和域名过滤一下”。它很快给出了改进版。优化思路主要有三条。

第一,用Content-Type过滤。真正的视频响应,响应头里往往会出现video/mp4、application/vnd.apple.mpegurl这类媒体类型,而不仅仅是 URL 里的关键词。Playwright 的request.response().headers可以拿到响应头,不过代价是需要把监听事件从request改成response,因为只有等响应回来了才知道类型。

第二,用域名白名单缩小范围。有些 CDN 域名一眼就能认出跟视频有关,比如包含 “video”“vod”“cdn” 字样;也有的非常泛,需要结合页面实际情况人工确认。让 Claude Code 把捕获结果里出现次数较多的域名单独列出来,再决定保留哪些。

第三,对查询参数做归并。同一个视频地址可能因为时间戳不同,被请求了两次,只需要保留第一次或者最后一次的结果。优化后的脚本会按去掉查询参数之后的主路径进行去重。

经过这三轮调整,脚本的输出已经从几十条噪点数据,简化到了两三条真正能用于下载的地址。这个过程看起来简单,但如果没有 Claude Code 帮助,我可能要连续翻好几个小时网络协议相关的文档,才能找到合适的过滤方式。

4.4 第四步:下载模块与重试机制

地址拿到手之后,最后一步是下载。基础版下载函数其实非常简单:

import requests def download_video(url: str, save_path: str, headers: dict | None = None) -> None: send_headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/124.0 Safari/537.36" } if headers: send_headers.update(headers) with requests.get(url, headers=send_headers, stream=True, timeout=30) as r: r.raise_for_status() total_size = int(r.headers.get("Content-Length", 0)) saved_size = 0 with open(save_path, "wb") as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) saved_size += len(chunk) print(f"保存完成: {save_path},大小约 {saved_size / 1024 / 1024:.2f} MB")

这里面有个容易忽略的点:请求头里的 Referer。很多视频 CDN 会校验访问来源,如果请求头里缺少页面域名,服务器会直接返回 403。所以下载时我习惯把捕获页面时的地址作为 Referer 一起传进去。Claude Code 一开始生成的代码没有这层处理,我在实测环节发现 403 之后,让它补上了这个逻辑。

为了防止网络波动导致下载中断,我还加了一个简单的重试包裹函数。下载过程中如果某个分块读取超时,就重新发起请求并从头续传。这里没有做特别复杂的断点续传,但从工程角度看,对几十 MB 到一两 GB 的单文件视频,这个方案已经足够稳定。

5. 实测记录:从一个公开演示页面提取并下载视频

所有代码都写在纸上、跑在测试命令里,终究还是要在真实页面上验证一遍。我选了一个内部的公开演示页面来测,页面本身允许播放一段产品宣传视频,可以在页面里找到播放按钮。整体操作流程如下。

5.1 实际操作过程和关键参数调整

我先跑静态脚本试水:

python scripts/static_finder.py "https://demo.internal.example.com/showcase"

输出结果是空的,只打印了一个空列表。意料之中,因为页面主体由前端框架渲染,视频标签不在初始 HTML 里。

接着跑动态捕获脚本:

python scripts/dynamic_capture.py

这次拿到了一批地址,但其中一半是接口地址,还有一两个是图片地址。按上一节的说法,我对脚本做了两处改动:监听方式从request改成response,用响应头里的Content-Type作为过滤依据;加了域名白名单逻辑。第二次运行后,输出剩下两个地址,一个是.mp4主视频,另一个是.m3u8播放列表。

为什么一个页面会同时产生两种格式?现在的视频站多平台适配很普遍:浏览器端 HTML5 播放器用了 mp4 直链,另一套面向移动端 HLS 流的逻辑也加载了 m3u8 播放列表。如果你下载的目标对面都有浏览器适配能力,那 mp4 直链通常是最省事的;m3u8 则需要进一步处理切片文件。

我把 mp4 地址拿出来下载,一开始直接用了基础版本地保存,结果报了 403。检查后发现缺少 Referer,补上后仍然 403,再加了页面首次访问时的 Cookie,这次就成功了。这类防盗链机制在实践中非常常见,所以我在 utils.py 里统一维护了一份“下载请求头”配置,每次遇到 403 就往里面补字段,而不是每次单独在脚本里改。

5.2 常见问题与排查技巧实录

问题一:捕获到了地址,但下载下来不是视频文件。

可能是服务器返回了一个 JSON 字符串或 HTML 页面,提示请求无效。解决办法是先看响应体前几百字节的内容,而不是盲目保存。我会用一个简单的请求测试脚本,把响应前 200 字打出来判断真实类型。

问题二:地址带签名参数,过一段时间就失效。

很多 CDN 的地址在一小时内有效,最长一般不超过一天。下载逻辑必须在捕获地址之后尽快用,超过有效期再访问会返回 403 或 404。我的做法是整合成一个脚本,一边捕获一边下载,中间不做跨天保存。

问题三:m3u8 播放列表怎么处理?

m3u8 本身不是视频文件,而是一个文本清单,里面写着一串.ts视频切片地址。处理它的正规方式是用专门的流媒体下载工具,或者自己写一个分段下载器。我在本项目里只把 m3u8 地址保留在日志里,没有为它单独做完整下载链路,对普通单文件 mp4 场景,这样做已经够用。

问题四:Playwright 无头模式有时拿不到链接,但打开有头浏览器就能拿到。

部分网站会对无头浏览器做识别。排查方式是把headless=True改成headless=False,弹出一个真实浏览器窗口,观察页面行为差异。有时还要配合--disable-blink-features=AutomationControlled参数。Claude Code 在这类问题排查上帮了大忙,它知道那些不容易通过搜索找到的浏览器自动化细节。

一路实测下来,最深的感受是脚本本身并不神秘,真正考验人的是对网络协议和浏览器机制的理解。这两块基础不牢,代码写得再漂亮,面对 403、超时、格式判断的时候还是无从下手。

6. 使用边界与我的实操心得

写到这里,一个绕不开的话题是这种技术工具的使用边界。我非常清楚,任何能提取视频地址的工具,都不能被用来获取未经授权的、需要付费或者仅限登录用户访问的内容。我从一开始就给这个项目定了规则:只处理自己拥有权限的内容,包括自己部署的站点、内部授权页、公开的免费演示素材,以及明确允许下载的资源。对于需要登录凭据才能查看、需要额外付费解锁的视频,直接不碰,也不在脚本里加入任何绕过访问控制的逻辑。

6.1 适用范围和合规意识

具体来说,下列场景是安全的:调试自己的网页媒体加载路径;批量整理公司产品素材库;保存带合法授权的公开教学视频;在开发测试环境里做压力验证。这些行为的共同点是“你本来就有权访问和保存”。反过来,依靠提取地址去下载会员专属课程、盗版影视资源,不仅违背项目初衷,也可能触碰法律红线。

如果你打算把这个项目扩展成通用工具,建议多做一步:在脚本入口处加入一个确认提示,声明使用者的授权责任。这不只是形式,更是给长期维护的人一个明确的边界提示。我在项目 README 里就特别写了一段使用范围说明,避免几个月后连自己都忘了工具的边界在哪儿。

6.2 给后来者的一些实操经验

这些经验全部来自踩坑后的复盘,不经修饰直接分享给你。

第一,捕获地址和下载验证一定要分开写。把两个功能放在一个脚本里,看似方便,实际调试时非常痛苦。分开之后,哪一步出问题,一步就能定位。下载器里面加打印日志,记录状态码和响应类型,比事后瞎猜强得多。

第二,动态捕获的等待时间不要设太短。我之前设过 3 秒,结果播放器还没发出视频请求,脚本就关闭了浏览器,导致捕获结果为空。改成 8 到 10 秒后,基本没有再漏过。当然,具体等待时间要看页面复杂程度,视频播放器启动快的话 5 秒也够。

第三,多关注响应头而不是只看 URL。URL 像人的名字,可能有同名不同人;响应头里的Content-Type更像身份证,准确度更高。捕获过滤和下载验证都应该把Content-Type作为主要依据之一。

第四,请求头配置统一放公共模块。不要把 User-Agent、Referer 这些散落在每个脚本里,统一管理能减少大量无效调试。我在 utils.py 里放了一份默认请求头,后续任何脚本直接导入,遇到新网站的校验规则,也只需要在一个地方补字段。

最后说一个延伸方向。这套“捕获隐藏资源”的思路不只适用于视频,把关键词从.mp4换成其他特定资源类型,代码逻辑完全复用,就能变成一个通用的资源探测工具。我后来就用它排查过页面上误加载的高清大图路径,也算是意外的收获。如果你手头有类似网页资源定位的需求,完全可以从这个项目的代码改起,大概率比从零写要省一大半时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询