如何分析一个 TVBox 风格接口,并提取可读配置
本文记录一次对
http://fty.xxooo.cf/tv这类 TVBox 风格接口的结构分析过程,重点是 识别配置层、提取可读内容、判断依赖关系、完成入口层备份。
目标不是抓取视频文件本体,而是把一个“看起来不像普通网页”的接口还原成可研究、可归档、可迁移的配置结构。
一、为什么要分析这类接口
很多 TVBox / 影视壳接口,表面上是一个 URL:
http://fty.xxooo.cf/tv
把它填进 App 之后,App 就能显示搜索源、影视分类、直播源、壁纸、播放器行为等内容。
但这类地址往往不是:
- 普通 HTML 页面
- 明文 JSON 接口
- 一眼就能看懂的资源站清单
更常见的实际情况是:
- 页面里藏了一段很长的编码字符串
- 编码字符串解开后才是真正的配置对象
- 配置对象里继续引用外部脚本、外部站点、外部直播清单
所以,如果你的目标是:
- 判断这个接口到底是不是配置入口
- 把配置内容备份下来
- 分析哪些东西是你能迁移到自己网站的
- 弄清楚它依赖了哪些外部服务
那么你需要做的,不是“直接复制网页”,而是做一次 接口结构解析。
二、分析目标与边界
这篇教程做的是:
- 抓取接口返回内容
- 识别其中是否包含隐藏配置
- 抽取并还原配置对象
- 清洗成可解析的 JSON
- 统计核心字段
- 判断哪些是入口层,哪些仍然依赖外部节点
这篇教程不讨论:
- 如何抓取视频文件本体
- 如何批量还原第三方实际视频资源
- 如何复制第三方受版权保护内容并继续分发
更准确的说,这是一篇:
TVBox 接口配置层分析与备份教程
三、准备工作
本次分析在 Linux 环境中完成,使用的是 Python 3。
你至少需要:
python3- 能发起 HTTP 请求的基础库
- 能做文本处理和 JSON 解析
如果你只想快速复现,下面这几个 Python 标准库就够用了:
import re
import ssl
import json
import base64
import urllib.request
四、第一步:确认它不是普通网页
先抓取接口原始内容。
示例代码:
import ssl
import urllib.request
url = 'http://fty.xxooo.cf/tv'
ua = 'Mozilla/5.0'
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
req = urllib.request.Request(url, headers={'User-Agent': ua})
with urllib.request.urlopen(req, timeout=20, context=ctx) as r:
page = r.read()
print('bytes:', len(page))
print(page[:200])
如果你直接把结果当网页看,通常会发现:
- 页面内容不可读
- 不是正常的 HTML 文档结构
- 里面夹杂了一大段超长字符
这时候就要开始怀疑:
页面里可能嵌了 base64 配置体。
五、第二步:在页面里找出长 base64 串
这类接口最常见的壳做法,是把真正配置内容塞进一段超长 base64 字符串里。
可以直接用正则去找:
import re
text = page.decode('utf-8', 'ignore')
candidates = sorted(
re.finditer(r'[A-Za-z0-9+/=]{500,}', text),
key=lambda m: len(m.group(0)),
reverse=True,
)
print('candidate count:', len(candidates))
print('longest length:', len(candidates[0].group(0)))
这里的核心思路是:
- base64 字符集比较固定
- 真正的配置串通常很长
- 所以优先取页面里最长的候选段
在这次样本里,确实找到了一个超长候选串,而且解码后明显能看到类似 JSON 的结构。
六、第三步:base64 解码,看它到底是不是配置
把候选字符串做 base64 解码:
import base64
blob = candidates[0].group(0)
decoded = base64.b64decode(blob + '==', validate=False).decode('utf-8', 'ignore')
print(decoded[:500])
这一步如果成功,你通常会看到类似这样的东西:
{
"spider": "...",
"wallpaper": "...",
"sites": [ ... ],
"lives": [ ... ]
}
一旦出现这些关键字段,基本就可以确定:
这不是普通网页,而是一个被包在页面里的配置对象。
七、第四步:为什么直接 json.loads() 会失败
这是这类接口最常见的坑之一。
你以为已经拿到 JSON 了,结果直接解析会报错,例如:
json.decoder.JSONDecodeError: Expecting value
原因通常不是“内容错了”,而是:
- 配置看起来像 JSON
- 但实际上混入了 JavaScript 风格注释
- 比如:
//{"key":"光影","name":"🌞光影┃多线", ... }
JSON 标准本身不允许这种注释行。
所以你必须先清洗,再解析。
八、第五步:清理注释,把非标准 JSON 变成可解析 JSON
最简单的处理方式,是先按行扫描,把整行 // 注释去掉。
示例代码:
import re
import json
clean = '\n'.join(
'' if re.match(r'^\s*//', line) else line
for line in decoded.splitlines()
)
config = json.loads(clean)
这样做之后,配置就能顺利读出来。
这一步的意义非常大,因为它说明:
- 这个接口的“内容层”是可以恢复的
- 失败点只是格式兼容,而不是配置损坏
- 后面你可以把它保存成自己的可读配置样本
九、第六步:把结果落盘,方便后续分析
建议至少保存四份文件:
1. 原页面文本
from pathlib import Path
outdir = Path('tmp/tvbox_probe_fty_xxooo_cf_tv')
outdir.mkdir(parents=True, exist_ok=True)
(outdir / 'raw_page.txt').write_text(text, encoding='utf-8', errors='ignore')
2. 解码后的原始文本
(outdir / 'decoded_raw.txt').write_text(decoded, encoding='utf-8')
3. 清洗后的 JSON
(outdir / 'decoded_clean.json').write_text(
json.dumps(config, ensure_ascii=False, indent=2),
encoding='utf-8'
)
4. 统计摘要
summary = {
'site_count': len(config.get('sites', []) or []),
'live_count': len(config.get('lives', []) or []),
'parse_count': len(config.get('parses', []) or []),
'spider': config.get('spider'),
'wallpaper': config.get('wallpaper'),
}
(outdir / 'summary.json').write_text(
json.dumps(summary, ensure_ascii=False, indent=2),
encoding='utf-8'
)
这样一来,你后面写文章、做截图、做 diff、做归档都会轻松很多。
十、第七步:读懂配置结构
从这次样本里,最关键的顶层字段包括:
spiderwallpapersiteslivesparses(这次样本里为 0)
1. sites
这是最核心的部分,表示各种站点源、规则源、搜索源、聚合源。
本次样本里:
sites总数:50- 可搜索站点:35
quickSearch站点:28
常见形式包括:
csp_LibvioGuardcsp_WoGGGuardcsp_NewCzGuardcsp_AppSxGuardcsp_BiliGuard- 远程
drpy2.min.js
说明它并不是“一个后端”,而是很多 resolver/规则混合在一起。
2. lives
表示直播或导入类资源。
本次样本里:
lives数量:7
例如:
- 外部 m3u 清单
- 外部 txt 接口
- 平台直播合集
3. wallpaper
通常是界面壁纸或展示资源地址。
这意味着:
- 接口不仅控制源
- 还可能控制 UI 展示体验
4. spider
这是这类配置里很值得重点关注的字段。
这次样本中它长这样:
https://oss4liview.moji.com/thd_file/2026/05/14/7443fdcc54d4aebef5998487590ed758.jpg;md5;0370a162a1edc618a5fe426160f8cf89
它看起来像图片地址,但又带 ;md5;... 标记。
这通常说明:
- 它不是单纯的展示图片
- 更像某种依赖包/组件入口的伪装包装
- 运行方需要按自己的规则去识别和加载它
这类字段要格外小心,因为:
你即使备份了 JSON,也未必真正掌握了它背后的依赖逻辑。
十一、第八步:统计 API 家族,判断这是不是聚合入口
一个很有价值的分析动作,是统计 sites[].api 出现频率。
示例代码:
site_api_counts = {}
for s in config.get('sites', []) or []:
if not isinstance(s, dict):
continue
api = s.get('api') or '(none)'
site_api_counts[api] = site_api_counts.get(api, 0) + 1
print(json.dumps(site_api_counts, ensure_ascii=False, indent=2))
这次样本中,较明显的分布包括:
csp_AppSxGuard× 7csp_BiliGuard× 7csp_T4Guard× 2- 远程
drpy2.min.js× 3 - 其余很多都是单站点专属 resolver
这说明它本质上是:
- 一组内置规则
- 一组第三方站点适配器
- 一组远程 JS 驱动规则
- 一堆外部依赖拼起来的聚合入口
所以它更像:
配置分发层 / 聚合入口层
而不是:
完整自包含的视频资源站
十二、第九步:提取所有外部依赖域名
如果你后面想“迁移到自己网站”,这一部分非常关键。
你必须知道:
- 哪些资源在你自己手里
- 哪些东西仍然指向外部
可以遍历整个配置对象,把所有 URL 抽出来再提域名:
from urllib.parse import urlparse
urls = []
def walk(x):
if isinstance(x, dict):
for v in x.values():
walk(v)
elif isinstance(x, list):
for v in x:
walk(v)
elif isinstance(x, str) and x.startswith(('http://', 'https://')):
urls.append(x)
walk(config)
domains = sorted({urlparse(u).netloc for u in urls})
for d in domains:
print(d)
这次样本里直接出现的外部域名包括:
oss4liview.moji.com深色壁纸.xxooo.cfwww.libvio.runwww.xb6v.comgit.yylx.wingh-proxy.comgh.927223.xyzsub.ottiptv.ccnos.netease.com107.173.211.148bdcache1-f1.v3mh.comdiyp5.112114.xyzepg.112114.xyz
这一步能直接帮你得出一个重要结论:
即使你把配置文件搬到了自己网站,真正的内容能力仍然依赖很多外部节点。
十三、第十步:给这些源做能力分组
为了后续写文章或继续维护,建议把 sites 分成几个类别。
例如:
1. 网盘 / 聚合 / 推送类
- 云盘
- 聚盘搜
- 盘搜类
- 手机推送类
2. 影视搜索 / 秒播 / 多线类
- Libvio 类
- WoGG 类
- Cz 类
- 多线 AppSx 类
- 磁力类
3. 动漫 / 音乐 / 听书 / 教育类
- 动漫站
- 音乐源
- 听书源
- 儿童/教育内容
4. 直播 / 平台内容类
- 虎牙
- 斗鱼
- B 站合集
- 体育直播
这一步的价值在于:
- 你不再只是“拿到一个大 JSON”
- 而是知道它在产品层面上都装了些什么
十四、第十一步:判断什么能迁,什么不能
这是整篇分析里最重要的判断部分。
可以迁移 / 备份的部分
这些内容适合做你自己的入口层:
- 清洗后的配置 JSON
- 你自己编写的说明页
- 你自己整理的分类说明
- 你自己托管的图标/壁纸/封面
- 你自己的版本记录和变更日志
只能继续外部引用的部分
这些即使你换成自己域名,也通常还是依赖别人:
sites中第三方站点的实际搜索/播放能力- 外部
drpy2.min.js - 外部
*.js规则文件 - 外部
m3u/txt直播清单 - 外部
wallpaper spider所代表的依赖逻辑
不应误判为“已经迁移完成”的部分
以下情况很容易让人误会,但技术上并不成立:
- 只是把原配置 JSON 换到自己域名
- 就说整个站都被迁移了
- 或者把所有内容都说成“自己提供”
更准确的说法应该是:
迁移成功的是“配置入口层”,不是“视频资源本体层”。
十五、第十二步:写成适合自己网站发布的结论
如果你要把这次分析发到网站里,建议把结论写清楚:
推荐结论写法
这类接口本质是一个 TVBox 风格的聚合配置入口。
页面本身并不是普通网页,而是内嵌了一段经过编码包装的配置对象。
通过提取、解码、清理注释和 JSON 化处理,可以恢复出它的 sites、lives、spider、wallpaper 等结构。
但需要强调的是:
- 可备份的是配置层
- 可迁移的是入口层
- 真正的资源能力依然大量依赖外部源、外部脚本和外部直播清单
因此,这次分析更适合作为:
- 接口结构研究
- 配置备份
- 自建入口参考
- 后续差异监控基线
而不是把它误认为“完整独立站点复制”。
十六、完整示例脚本
下面给一个可直接跑的简化版示例,把前面步骤串起来:
#!/usr/bin/env python3
import re
import ssl
import json
import base64
import urllib.request
from pathlib import Path
from urllib.parse import urlparse
URL = 'http://fty.xxooo.cf/tv'
UA = 'Mozilla/5.0'
OUTDIR = Path('tmp/tvbox_probe_fty_xxooo_cf_tv')
OUTDIR.mkdir(parents=True, exist_ok=True)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
req = urllib.request.Request(URL, headers={'User-Agent': UA})
with urllib.request.urlopen(req, timeout=20, context=ctx) as r:
page = r.read()
text = page.decode('utf-8', 'ignore')
(OUTDIR / 'raw_page.txt').write_text(text, encoding='utf-8', errors='ignore')
candidates = sorted(
re.finditer(r'[A-Za-z0-9+/=]{500,}', text),
key=lambda m: len(m.group(0)),
reverse=True,
)
if not candidates:
raise RuntimeError('未找到长 base64 候选串')
blob = candidates[0].group(0)
decoded = base64.b64decode(blob + '==', validate=False).decode('utf-8', 'ignore')
(OUTDIR / 'decoded_raw.txt').write_text(decoded, encoding='utf-8')
clean = '\n'.join(
'' if re.match(r'^\s*//', line) else line
for line in decoded.splitlines()
)
config = json.loads(clean)
(OUTDIR / 'decoded_clean.json').write_text(
json.dumps(config, ensure_ascii=False, indent=2),
encoding='utf-8'
)
urls = []
def walk(x):
if isinstance(x, dict):
for v in x.values():
walk(v)
elif isinstance(x, list):
for v in x:
walk(v)
elif isinstance(x, str) and x.startswith(('http://', 'https://')):
urls.append(x)
walk(config)
domains = sorted({urlparse(u).netloc for u in urls})
summary = {
'url': URL,
'site_count': len(config.get('sites', []) or []),
'searchable_sites': sum(1 for s in config.get('sites', []) if isinstance(s, dict) and s.get('searchable') == 1),
'quick_search_sites': sum(1 for s in config.get('sites', []) if isinstance(s, dict) and s.get('quickSearch') == 1),
'parse_count': len(config.get('parses', []) or []),
'live_count': len(config.get('lives', []) or []),
'spider': config.get('spider'),
'wallpaper': config.get('wallpaper'),
'domains': domains,
}
(OUTDIR / 'summary.json').write_text(
json.dumps(summary, ensure_ascii=False, indent=2),
encoding='utf-8'
)
print(json.dumps(summary, ensure_ascii=False, indent=2))
十七、本文实战结果摘要
在这次对 http://fty.xxooo.cf/tv 的实际分析中,最终得到了这些结论:
- 它不是普通网页,而是配置入口
- 页面中嵌了超长 base64 配置体
- 解码后得到的是非严格 JSON
- 失败原因主要是其中混有
//注释 - 清洗后可正常解析
- 提取出的配置包含:
- 50 个
sites - 35 个可搜索源
- 28 个
quickSearch源 - 7 个
lives
- 50 个
- 配置内直接出现了多组外部域名依赖
- 因此更适合被理解为“聚合入口层”而不是“完整资源站”
十八、最后的建议
如果你也遇到类似接口,最稳的分析顺序通常是:
- 先判断是不是普通网页
- 再找有没有编码配置体
- 能解码就先落盘
- JSON 失败先检查是不是注释或轻度混淆
- 把顶层结构和外部依赖跑清楚
- 最后再判断它到底是:
- 配置入口
- 聚合导航
- 规则分发层
- 还是完整后端
很多时候,真正值得长期保存的,不是“某一时刻能不能打开”,而是:
你有没有把它的结构、依赖和可迁移部分搞清楚。
这才是后面做备份、做差异追踪、做自建入口的基础。




Comments | NOTHING