From f9723f3cf1f93b2a26c08800fafb717a8a1fdf4d Mon Sep 17 00:00:00 2001 From: Harold <8866033@gmail.com> Date: Fri, 24 Apr 2026 19:58:25 +0800 Subject: [PATCH] docs: add FKTV spider design --- .../specs/2026-04-24-fktv-spider-design.md | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 py/docs/superpowers/specs/2026-04-24-fktv-spider-design.md diff --git a/py/docs/superpowers/specs/2026-04-24-fktv-spider-design.md b/py/docs/superpowers/specs/2026-04-24-fktv-spider-design.md new file mode 100644 index 0000000..d0e2244 --- /dev/null +++ b/py/docs/superpowers/specs/2026-04-24-fktv-spider-design.md @@ -0,0 +1,292 @@ +# FKTV Python 爬虫设计 + +## 目标 + +在当前 Python 仓库中新增一个符合 `base.spider.Spider` 接口的凡客TV爬虫,行为参考用户提供的 FKTV JS 版本,但实现形式遵循仓库现有的单文件 Spider 约定。 + +本次实现范围: + +- 首页分类 +- 分类浏览 +- 搜索 +- 详情解析 +- 多线路剧集组装 +- 播放接口直链优先 +- 接口失败时回退详情页 + +本次不实现: + +- 弹幕 +- 页面嗅探 +- 本机 `curl` 回退 +- 验证码绕过 + +## 方案选择 + +采用“详情页解析状态,播放时调用官方切换接口”的方案,而不是只依赖详情页内嵌 `play_links`。 + +原因如下: + +- FKTV 的剧集和线路是两套独立状态,详情页内嵌 `play_links` 可能只对应当前默认剧集 +- 如果只复用默认 `play_links`,用户切换到别的剧集时容易串集 +- 参考实现的核心可靠性来自 `link_id + line_id` 组合后再请求官方播放接口 +- 在当前 Python 仓库里保留接口直链优先、失败回退页面,已经覆盖核心需求,不必移植 OmniBox 专属能力 + +## 模块边界 + +新增 `py/凡客TV.py`,不修改 `py/base/`。 + +模块对外实现: + +- `init` +- `getName` +- `homeContent` +- `homeVideoContent` +- `categoryContent` +- `searchContent` +- `detailContent` +- `playerContent` + +模块内部 helper 负责: + +- 请求头与 cookie 管理 +- HTML / JSON 请求 +- 列表卡片解析 +- 详情页状态脚本解析 +- 线路 tab 提取 +- `play_id` 编解码 +- 播放接口请求与结果归一化 +- 直链与回退结果构造 + +## 站点策略 + +固定站点参数: + +- host:`https://fktv.me` +- 固定桌面端 `User-Agent` +- 固定 cookie:`_did=57nTmEknMZ146xw4KXGHDCHk1MjshRyY` + +请求策略: + +- 列表、搜索、详情页使用普通 GET +- 播放切换接口使用 POST +- 播放接口请求头必须包含: + - `User-Agent` + - `Referer` + - `Origin` + - `X-Requested-With` + - `Content-Type: application/x-www-form-urlencoded; charset=UTF-8` + - 固定 cookie + +## 分类与首页 + +`homeContent` 返回固定分类: + +- `1` 电影 +- `2` 剧集 +- `4` 动漫 +- `3` 综艺 +- `8` 短剧 +- `6` 纪录片 +- `7` 解说 +- `5` 音乐 + +`homeVideoContent` 不单独抓推荐,返回空列表,和仓库中多个站点保持一致。 + +分类 URL 固定为: + +- `/channel?page={page}&cat_id={type_id}&page_size=32&order=new` + +分类页解析规则: + +- 优先从 `.meta-wrap` 所在卡片提取数据 +- 兜底解析 `.hover-wrap` +- 提取详情链接中的 `/movie/detail/{id}` +- 提取标题、封面、标签和备注 +- `vod_id` 保留短 ID,不返回完整 URL + +返回结构遵循仓库当前约定: + +- `page` +- `limit` +- `total` +- `list` + +不返回 `pagecount`。 + +## 搜索 + +搜索 URL 固定为: + +- `/search?keyword=` + +搜索页解析复用分类卡片逻辑: + +- 提取详情短 ID、标题、封面、标签 +- 去重后返回 +- 关键字为空时直接返回空列表 + +搜索返回: + +- `page` +- `limit` +- `total` +- `list` + +不返回 `pagecount`。 + +## 详情解析 + +详情页 URL 固定为: + +- `/movie/detail/{vod_id}` + +详情页需要同时解析 HTML 可见信息和页面脚本状态。 + +基础元信息: + +- `vod_id` +- `vod_name` +- `vod_pic` +- `vod_content` +- `vod_remarks` + +页面脚本状态: + +- `movieId` +- `linkId` +- `links` +- `play_links` +- `play_error_type` + +其中: + +- `links` 代表剧集列表,是详情页组装剧集的主数据源 +- `play_links` 仅用于诊断和线路兜底,不作为目标剧集的真值来源 + +线路提取规则: + +- 优先从带 `data-line` 的线路 tab 提取线路 ID 和线路名 +- 如果页面没有线路 tab,则退化使用 `play_links` 中的线路 ID 和名称 +- 如果两者都没有,则详情页返回空播放字段 + +剧集提取规则: + +- 逐个读取 `links` 中的剧集项 +- 每个剧集至少提取 `id` 和展示名 +- 如果没有名称,则按 `name`、`title`、`id` 顺序兜底 + +详情页播放字段组装规则: + +- `vod_play_from` 为线路名称列表,使用 `$$$` 拼接 +- `vod_play_url` 为每条线路下的剧集列表,使用 `#` 拼接单集、`$$$` 拼接线路 +- 每个单集条目格式为 `剧集名$play_id` + +这里会为每条线路复用同一批剧集。原因是 FKTV 的线路和剧集是分离状态: + +- 剧集由 `links` 定义 +- 线路由线路 tab 或 `play_links` 定义 +- 真正播放地址必须在 `playerContent` 中通过 `link_id + line_id` 实时换取 + +`vod_remarks` 只保留少量站点状态提示: + +- `play_error_type == captcha` 时提示站点需要验证码 +- `play_error_type == need_vip` 时提示站点存在 VIP 限制 + +## Play ID 设计 + +`play_id` 不直接保存页面 URL,而是编码为一个紧凑 JSON 字符串,至少包含: + +- `movie_id` +- `link_id` +- `line_id` +- `line_name` +- `episode_name` +- `type` +- `page` + +设计目标: + +- 播放时能准确定位目标剧集和目标线路 +- 即使详情阶段只拿到短 ID,也能回退到详情页 +- 在测试里可以稳定断言 `play_id` 的结构和往返解码行为 + +## 播放解析 + +`playerContent` 分三层处理。 + +第一层,直接透传: + +- 如果传入就是 `.m3u8`、`.mp4`、`.flv` 等明显媒体直链,直接返回 `parse=0` + +第二层,播放接口直链优先: + +- 解码 `play_id` +- 回读详情页,重新获取当前 `movieId`、默认 `linkId` 和页面状态 +- 使用 `play_id` 中的 `link_id` 作为目标剧集,缺失时回退页面默认 `linkId` +- POST `https://fktv.me/movie/detail/{movie_id}` +- 表单固定为 `link_id={link_id}&is_switch=1` +- 从响应 JSON 的 `data.play_links` 中提取各线路直链 +- 如果指定了 `line_id`,则只返回对应线路 +- 如果没有指定 `line_id`,则返回该剧集下接口给出的全部线路 + +第三层,失败回退: + +- 接口没有可播地址 +- 接口返回 `need_vip` +- `play_id` 非法但还能推导出详情页 +- 详情页能打开但无法换取直链 + +这些场景统一返回 `parse=1`,回退到详情页地址。 + +## 播放结果规范 + +直链结果: + +- `parse=0` +- `playUrl=""` +- `url` 为首个可播地址 +- `header` 至少包含: + - `User-Agent` + - `Referer` + - `Origin` + +当接口返回多线路时: + +- 额外返回 `urls` +- 每项格式为 `{"name": "<线路名>", "url": "<直链>"}` + +回退结果: + +- `parse=1` +- `playUrl=""` +- `url` 为详情页地址 +- `header` 带页面请求所需的 `User-Agent` 和 `Referer` + +不实现页面嗅探,不返回 OmniBox 风格的额外字段。 + +## 测试策略 + +新增 `py/tests/test_凡客TV.py`,全部使用离线 HTML / JSON fixture 和 mock,不做真实联网。 + +重点覆盖: + +- `homeContent` 返回固定分类 +- `categoryContent` 正确构造 `/channel` URL 并解析卡片 +- `searchContent` 正确构造 `/search` URL,空关键字直接返回空列表 +- 详情页状态解析:`movieId`、`linkId`、`links`、`play_links`、`play_error_type` +- 线路 tab 提取和剧集名称兜底 +- `detailContent` 组装 `vod_play_from` / `vod_play_url` +- `play_id` 编解码往返一致 +- `playerContent` 对直链直接返回 +- `playerContent` 请求官方切换接口并按 `line_id` 过滤 +- `playerContent` 在线路未指定时返回全部接口线路 +- `playerContent` 在 `need_vip`、空结果、非法标识等场景下回退详情页 + +## 风险与边界 + +- FKTV 详情页里的脚本状态依赖站点当前模板,本次只兼容参考结构 +- 固定 cookie 未来可能失效,失效后播放接口可能退化为页面回退 +- 如果站点把线路 tab 或脚本变量名称整体改版,详情组装和播放接口都需要同步调整 +- 不实现验证码处理,遇到 `captcha` 只给出提示并回退页面 +- 不做 `curl` 回退和页面嗅探,意味着接口彻底失效时只能降级到页面解析