docs: add cupfox spider design

This commit is contained in:
Harold
2026-04-19 19:57:05 +08:00
parent 0848dbf05b
commit f99824ee40
@@ -0,0 +1,336 @@
# 茶杯狐 Python 爬虫设计
## 目标
在当前 Python 仓库中新增一个符合 `base.spider.Spider` 接口的茶杯狐站点爬虫,完整覆盖以下能力:
- 首页分类
- 首页推荐
- 分类列表
- 搜索
- 详情解析
- 播放解析
- 搜索/详情/播放页面的人机验证绕过
实现基于站点 HTML 页面和 `foxplay` 接口,不引入服务端路由层,不修改 `base/` 公共层,并保持当前仓库的短 ID 和结果结构约定。
## 范围
本次实现包含:
- 新增独立脚本,文件名为 `茶杯狐.py`
- 新增对应单元测试 `tests/test_茶杯狐.py`
- 使用单一站点主域:`https://www.cupfox.ai`
- 支持 `home/homeVideo/category/search/detail/player` 主链路
- 实现命中验证页后的 Cookie 合并、`token` 提取和 `/robot.php` 过盾请求
- 实现 `foxplay/api.php` 的两种播放链接解密
- 过滤 404 占位、403 和明显不可用链接,并在失败时回退 `parse=1`
本次实现不包含:
- 多域名探活与自动切换
- 浏览器自动化、JS 引擎执行和验证码识别
- 站外搜索源聚合和非站内播放链路修复
- 登录态管理和本地缓存持久化
## 方案选择
采用仓库现有的“单站点单文件 + 单测”方案:
- 对外保持 `Spider` 接口兼容
- 对内拆分为短 ID 处理、请求与过盾、卡片解析、详情解析、播放解密几个 helper
- 只在站点文件内部维护逻辑,不修改 `base/`
不直接照搬参考 JS 路由层的原因是:
- 当前仓库只消费 Spider 接口,不消费 Node 路由插件
- Python 版已有稳定的 HTML 爬虫实现风格,应延续既有结构
- 本次重点是把茶杯狐能力转成可测试的 Python Spider,而不是保留一层额外运行时
## 模块边界
新增脚本内部职责拆分如下:
- `init`
- 初始化主域、请求头、分页大小和字符映射表
- `homeContent`
- 返回分类列表和静态筛选空结构
- `homeVideoContent`
- 请求首页并解析推荐卡片,去重后截断
- `categoryContent`
- 请求分类页,复用卡片解析并返回页码、总量估算和列表
- `searchContent`
- 请求搜索页,复用卡片解析并支持 `quick`
- `detailContent`
- 请求详情页,提取元数据和多线路播放列表
- `playerContent`
- 请求播放页、提取 `player_aaaa`、调用 `foxplay/api.php`、解密直链并决定是否回退解析
- 私有辅助函数
- URL 补全与短 ID 编解码
- HTML 请求与过盾流程
- Cookie 合并
- `token` 提取和过盾加密
- `player_aaaa` 解析
- 两套播放解密
- 占位/禁播链接识别
## ID 与 URL 设计
遵循当前仓库“短 ID,不暴露完整 URL”的约定。
详情页短 ID
- 页面链接 `/movie/<slug>.html`
- 存储为 `detail/<slug>`
播放页短 ID
- 页面链接 `/play/<slug>.html`
- 存储为 `play/<slug>`
编码与解码原则:
- 只接受站内标准详情/播放路径
- 解析失败时返回空字符串,调用侧直接回空结果或回退
- 内部统一通过 `urljoin` 补全相对路径
## Host 与请求策略
本次只实现单域:
- `https://www.cupfox.ai`
统一请求头至少包含:
- `User-Agent`
- `Referer`
- `Accept`
- `Accept-Language`
请求原则:
- HTML 请求超时固定为 15 秒
- 普通页面请求走 `self.fetch` / `self.post`
- 页面请求默认不自动依赖持久会话,而是在单次过盾流程里显式维护 Cookie 字典
- 命中验证时只处理当前请求,不做全局 Cookie 缓存
- 请求失败时返回空结果或播放页回退,不抛出未处理异常
## 过盾设计
页面请求统一走 `_request_with_firewall()`
1. 首次请求目标 URL,收集响应文本和 `set-cookie`
2. 如果页面不包含 `人机验证``verifyBox`,直接返回 HTML
3. 如果命中验证,从 HTML 中提取 `var token = encrypt("...")`
4. 对当前 URL 和原始 token 分别执行参考实现中的字符位移 + 随机填充 + Base64 加密
5.`/robot.php` 发起表单 POST,请求头带上已收集 Cookie
6. 合并验证接口返回的 Cookie
7. 带最新 Cookie 二次请求原页面
8. 若二次请求仍是验证页,则按失败处理
该流程只用于 HTML 页面:
- 首页
- 分类页
- 搜索页
- 详情页
- 播放页
`foxplay/api.php` 不走过盾流程。
## 分类、首页与搜索设计
### 分类
分类从首页导航 `nav.bm-item-list a` 动态解析,提取:
- `type_id`
- `type_name`
只保留 `/type/<id>.html` 结构的导航链接。
### 首页推荐
首页推荐从 `.mobile-main .panel .movie-list-item` 提取,按详情 URL 去重后返回前 20 条。
### 分类列表
分类页路径:
- `/type/<type>-<page>.html`
每张卡片提取:
- 链接:`a[href]`
- 标题:`a[title]`
- 封面:`.Lazy[data-original]`
- 备注:`.movie-item-note`,为空时退回 `.movie-item-score`
### 搜索
搜索页路径:
- `/search/<keyword>----------<page>---.html`
搜索结果按 `.vod-search-list .box` 解析,字段提取规则与分类卡片一致,但标题优先取 `.movie-title`
返回策略:
- `limit` 固定为 20
- 列表和搜索返回 `page``total``limit``list`
- 不返回 `pagecount`
- 当页结果为空时 `total` 为 0
- 当页结果非空时,`total` 至少保证大于等于 `page * len(list)`,避免上层立即判定无后续分页
## 详情页设计
详情页解析以下字段:
- `vod_id`
- `vod_name`
- `vod_pic`
- `vod_content`
- `vod_year`
- `vod_director`
- `vod_actor`
- `vod_play_from`
- `vod_play_url`
解析策略:
- 标题取 `h1.movie-title`
- 海报取 `.poster img`
- 简介取 `.summary.detailsTxt` 的纯文本,并移除展开按钮文本
- 年份从 `.scroll-content a` 中提取四位数字
- 导演和演员从 `.info-data` 中按标签文案识别并拼接
播放列表策略:
- 线路名从 `.play_source_tab .swiper-slide` 提取
- 每个 `.play_list_box` 对应一组线路
- 集数条目从 `.content_playlist li a` 提取为 `名称$play/<slug>`
- 最终按仓库约定拼接成 `vod_play_from``vod_play_url`
如果线路数和列表数不一致:
- 优先保留已有剧集列表
- 缺失的线路名回退为 `线路<n>`
## 播放解析设计
### 播放页提取
`playerContent` 先还原播放页 URL,请求 HTML,再从脚本中提取:
- `player_aaaa.url`
- `player_aaaa.from`
- `player_aaaa.server`
如果找不到 `player_aaaa.url`,直接回退:
- `parse = 1`
- `url = 播放页 URL`
- `header``User-Agent` 和当前页 `Referer`
### `foxplay/api.php`
`player_aaaa.url` 存在时:
1.`vid=<player_aaaa.url>` POST 到 `/foxplay/api.php`
2. 若接口返回 `data.url`,按 `urlmode` 解密:
- `1``Decode1.sign`
- `2``decode2`
- 其他值直接使用原始 `url`
3. 对解密结果做占位链接识别
### 解密规则
`Decode1.sign` 对应参考实现的三段式处理:
- 自定义异或解码
- Base64 还原映射表
- 按明文/密文字母映射恢复真实路径
`decode2` 对应参考实现的字典反向位移逻辑:
- Base64 解码
- 每三个字符取中间位
- 按字典回退 3 位
### 占位链接识别
以下情况视为不可用直链:
- 空字符串
-`http(s)` / `//` / `magnet:` 链接
- 指向 `404.mp4`
- 参数含 `code=403`
- 包含 `forbidden`
### 播放返回策略
如果解密得到可用直链:
- 返回 `parse = 0`
- `url` 为真实地址
- `header.Referer` 指向 `muiplayer.php?vid=...`
如果 API 无直链、解密失败或命中占位链接:
- 返回 `parse = 1`
- `url` 为原播放页
- `header.Referer` 指向原播放页
## 错误处理
所有对外方法在站点异常时返回兼容结构,不抛出未处理异常:
- `homeContent` 返回空分类
- `homeVideoContent` 返回空列表
- `categoryContent` 返回当前页和空列表
- `searchContent` 返回当前页和空列表
- `detailContent` 返回空 `list`
- `playerContent` 返回原播放页的 `parse=1`
内部异常处理原则:
- 私有 helper 可抛出 `ValueError`
- 对外接口统一兜底
- JSON 解析、Base64 解码和映射解密失败时返回空字符串,不中断整个请求
## 测试设计
测试采用 `unittest``unittest.mock`,避免真实网络访问。
首批覆盖:
- 短 ID 编解码
- Cookie 合并和请求头拼装
- 验证页 `token` 提取和过盾二次请求
- 首页分类解析
- 首页推荐去重
- 分类列表解析
- 搜索结果解析和空关键词分支
- 详情页元数据和多线路播放列表拼装
- `Decode1``decode2` 的解密行为
- `playerContent` 的直链成功分支
- `playerContent` 的占位链接回退分支
- `playerContent` 的缺失 `player_aaaa` 回退分支
测试粒度要求:
- 先验证最小私有 helper
- 再验证对外接口方法
- 只 mock 网络层,不 mock 纯解析函数
## 验收标准
满足以下条件视为完成:
- 新增 `茶杯狐.py`,接口与仓库现有 Spider 一致
- 新增 `tests/test_茶杯狐.py`
- 列表和搜索结果使用短详情 ID
- 详情页播放列表使用短播放 ID
- 命中验证页时能完成两次请求和 Cookie 合并
- `playerContent` 能在可用时返回直链,在不可用时稳定回退
- 相关测试通过,且不引入 `pagecount` 到列表/搜索返回结构