最近在做一个兴趣项目,需要批量备份一些FC2上的公开视频。本以为写个简单的爬虫就能搞定,结果在解析视频源的过程中,接连遇到了好几道坎。从最初的requests库直接抓包,到最终折腾出一个还算稳定的在线服务(也就是现在的 twittervideodownloaderx.com/fc2_downloader_cn),中间踩了不少坑。这篇文章就把我的实战笔记整理出来,聊聊我在解决FC2视频下载时遇到的三个核心问题以及对应的技术方案,希望能给同样在搞视频流解析的朋友一些参考。
第一座大山:动态加载与源地址隐藏

打开一个FC2视频页面,比如 https://video.fc2.com/content/20250831yJ0Kry9C,按F12看Network,刷新后会发现一大堆JS和图片请求,但就是找不到MP4文件。这是因为视频地址压根没写在HTML里,而是通过JS动态加载的。
初期尝试: 我试图用requests直接拿HTML,然后用正则匹配视频地址。
最初的错误示范
import re
import requests
url = "https://video.fc2.com/content/20250831yJ0Kry9C"
resp = requests.get(url)
天真地以为能找到 .mp4
video_url = re.search(r'(https?://[^"\']+\.mp4)', resp.text)
print(video_url)
结果当然是打印出None。视频地址被藏在JS变量里,甚至可能被拆分、混淆。
解决方案: 放弃纯文本解析,转向模拟浏览器执行环境。我在后端集成了Playwright,启动一个Headless Chromium去完整加载页面,监听网络请求,直接从request和response中捕获视频流地址。
使用 Playwright 捕获视频请求(简化版)
from playwright.sync_api import sync_playwright
def capture_video_url(page_url):
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
extra_http_headers={"AcceptLanguage": "ja"}
)
page = context.new_page()
video_urls = []
监听请求,捕获包含 .mp4 或 .m3u8 的地址
def on_request(request):
if any(ext in request.url for ext in ['.mp4', '.m3u8']):
video_urls.append(request.url)
page.on("request", on_request)
page.goto(page_url, timeout=60000)
page.wait_for_timeout(5000) 等待视频开始加载
browser.close()
return video_urls[0] if video_urls else None
这种方式虽然慢一些(加载加等待约58秒),但成功率极高,能应对99%的JS动态加载情况。这也是工具解析需要几秒钟的原因之一。
第二座大山:防盗链与时效性签名
拿到视频地址只是第一步。把这个地址复制到浏览器直接打开,大概率会得到一个403 Forbidden。这是因为FC2的服务器会严格检查请求头中的Referer,必须来自video.fc2.com域。同时,很多视频地址后面都带有一串?token=...&expires=...参数,expires是一个时间戳,一旦过期,地址就失效了。
这意味着,解析动作和下载动作必须是连续的,不能中断。解析出来的地址不能保存下来过一会儿再用。
解决方案: 在服务端实现即时代理下载。当用户点击下载时,后端不是简单地把地址返回给浏览器,而是启动一个代理任务:
- 带着正确的
Referer和其他伪造头,向FC2服务器发起请求。 - 获取视频数据流。
- 边接收边将数据实时转发给用户。
这样做有两个好处:一是完美解决了Referer和Token时效问题,因为后端请求是实时的;二是可以在这个代理过程中做“手脚”,比如实现多线程分段下载来加速。
第三座大山:跨海速度与并发限制
FC2的服务器在日本,国内直连速度往往不理想。而且FC2对单个IP也有并发连接数限制。如果用户直接下载,不仅慢,还容易中断。
解决方案:构建分布式下载队列与分段加速
我在后端设计了一个基于Celery + Redis的任务队列,并部署了多个位于日本、韩国等地的下载节点。
当用户提交下载请求时:
- 任务分发:请求进入队列,调度器根据当前各节点的负载和健康情况,选择一个最优节点来处理。
- 云端分段拉取:节点收到任务后,向FC2服务器发起带有
Range头的分段请求,用多个连接同时拉取视频的不同部分。 - 合并回流:节点将分段获取的数据在内存中按顺序组装,并通过一个稳定的连接推送给用户。
核心的伪代码逻辑如下:
伪代码:下载节点上的分段拉取逻辑
import asyncio
import aiohttp
async def fetch_chunk(session, url, start, end):
headers = {'Range': f'bytes={start}{end}'}
async with session.get(url, headers=headers) as resp:
return await resp.read()
async def proxy_download(video_url, user_writer):
async with aiohttp.ClientSession() as session:
1. 获取文件总大小
head = await session.head(video_url)
total = int(head.headers['ContentLength'])
2. 分成4段并行获取
chunk_size = total // 4
tasks = []
for i in range(4):
start = i chunk_size
end = start + chunk_size 1 if i < 3 else total 1
tasks.append(fetch_chunk(session, video_url, start, end))
3. 等待所有段获取完成
chunks = await asyncio.gather(tasks)
4. 按顺序合并并写入用户响应
for chunk in chunks:
user_writer.write(chunk)
await user_writer.drain()
这种方式充分利用了FC2服务器的带宽和我们的节点带宽,实现了下载速度的叠加,也就是工具宣称的“速度提升300%”的技术基础。
总结:从代码到服务
把上面这些技术点——动态渲染爬虫、即时代理、分布式任务队列、分段加速——整合起来,就是一个完整的后端服务体系。而用户看到的前端页面,只是这个体系的冰山一角。
当初决定把这些能力开放出来,做成一个免费工具,也是因为发现很多人其实只需要一个简单可靠的下载入口,并不想(也没必要)自己去折腾Python脚本、Playwright和分布式架构。
如果你对这个过程中的某个技术细节感兴趣,或者有自己的优化方案,欢迎在评论区留言交流。技术就是在这样不断的“踩坑”和“填坑”中进步的。
浙公网安备 33010602011771号