如何分析一个 TVBox 风格接口,并提取可读配置

发布于 1 小时前  6 次阅读


如何分析一个 TVBox 风格接口,并提取可读配置

本文记录一次对 http://fty.xxooo.cf/tv 这类 TVBox 风格接口的结构分析过程,重点是 识别配置层、提取可读内容、判断依赖关系、完成入口层备份
目标不是抓取视频文件本体,而是把一个“看起来不像普通网页”的接口还原成可研究、可归档、可迁移的配置结构。


一、为什么要分析这类接口

很多 TVBox / 影视壳接口,表面上是一个 URL:

http://fty.xxooo.cf/tv

把它填进 App 之后,App 就能显示搜索源、影视分类、直播源、壁纸、播放器行为等内容。

但这类地址往往不是:

  • 普通 HTML 页面
  • 明文 JSON 接口
  • 一眼就能看懂的资源站清单

更常见的实际情况是:

  • 页面里藏了一段很长的编码字符串
  • 编码字符串解开后才是真正的配置对象
  • 配置对象里继续引用外部脚本、外部站点、外部直播清单

所以,如果你的目标是:

  1. 判断这个接口到底是不是配置入口
  2. 把配置内容备份下来
  3. 分析哪些东西是你能迁移到自己网站的
  4. 弄清楚它依赖了哪些外部服务

那么你需要做的,不是“直接复制网页”,而是做一次 接口结构解析


二、分析目标与边界

这篇教程做的是:

  • 抓取接口返回内容
  • 识别其中是否包含隐藏配置
  • 抽取并还原配置对象
  • 清洗成可解析的 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. 这个接口的“内容层”是可以恢复的
  2. 失败点只是格式兼容,而不是配置损坏
  3. 后面你可以把它保存成自己的可读配置样本

九、第六步:把结果落盘,方便后续分析

建议至少保存四份文件:

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、做归档都会轻松很多。


十、第七步:读懂配置结构

从这次样本里,最关键的顶层字段包括:

  • spider
  • wallpaper
  • sites
  • lives
  • parses(这次样本里为 0)

1. sites

这是最核心的部分,表示各种站点源、规则源、搜索源、聚合源。

本次样本里:

  • sites 总数:50
  • 可搜索站点:35
  • quickSearch 站点:28

常见形式包括:

  • csp_LibvioGuard
  • csp_WoGGGuard
  • csp_NewCzGuard
  • csp_AppSxGuard
  • csp_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 × 7
  • csp_BiliGuard × 7
  • csp_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.cf
  • www.libvio.run
  • www.xb6v.com
  • git.yylx.win
  • gh-proxy.com
  • gh.927223.xyz
  • sub.ottiptv.cc
  • nos.netease.com
  • 107.173.211.148
  • bdcache1-f1.v3mh.com
  • diyp5.112114.xyz
  • epg.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 化处理,可以恢复出它的 siteslivesspiderwallpaper 等结构。

但需要强调的是:

  • 可备份的是配置层
  • 可迁移的是入口层
  • 真正的资源能力依然大量依赖外部源、外部脚本和外部直播清单

因此,这次分析更适合作为:

  • 接口结构研究
  • 配置备份
  • 自建入口参考
  • 后续差异监控基线

而不是把它误认为“完整独立站点复制”。


十六、完整示例脚本

下面给一个可直接跑的简化版示例,把前面步骤串起来:

#!/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
  • 配置内直接出现了多组外部域名依赖
  • 因此更适合被理解为“聚合入口层”而不是“完整资源站”

十八、最后的建议

如果你也遇到类似接口,最稳的分析顺序通常是:

  1. 先判断是不是普通网页
  2. 再找有没有编码配置体
  3. 能解码就先落盘
  4. JSON 失败先检查是不是注释或轻度混淆
  5. 把顶层结构和外部依赖跑清楚
  6. 最后再判断它到底是:
    • 配置入口
    • 聚合导航
    • 规则分发层
    • 还是完整后端

很多时候,真正值得长期保存的,不是“某一时刻能不能打开”,而是:

你有没有把它的结构、依赖和可迁移部分搞清楚。

这才是后面做备份、做差异追踪、做自建入口的基础。


或许明日太阳西下倦鸟已归时