Files
tvboxzt/py/docs/superpowers/specs/2026-04-24-jinpai-spider-design.md
T
2026-04-24 16:23:34 +08:00

343 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 金牌 Python 爬虫设计
## 目标
在当前 Python 仓库中新增一个符合 `base.spider.Spider` 接口的金牌爬虫,行为参考用户提供的 JS 版本,覆盖以下能力:
- 首页分类与筛选
- 首页推荐
- 分类浏览
- 搜索
- 详情解析
- 播放解析
实现形式遵循当前仓库的单文件 Spider 约定和离线单测约定,不引入新的公共基类。
## 范围
本次实现包含:
- 新增独立脚本 `py/金牌.py`
- 新增独立测试 `py/tests/test_金牌.py`
- 实现带签名头的匿名 API 请求
- 动态拉取分类与筛选
- 首页返回热门推荐
- 分类支持类型、剧情、地区、语言、年份、排序筛选
- 搜索支持分页与空关键词保护
- 详情组装基础元数据与单线路剧集
- 播放接口返回多清晰度直链列表
本次实现不包含:
- 修改 `base/` 公共层
- 引入新的第三方依赖
- 真实联网集成测试
- 站点探活、容灾切换或缓存
- 参考 JS 的 HTTP 路由包装层
## 方案选择
采用“单文件 Spider + helper + 单测”的直接适配方案,而不是抽象新的签名 API 基类。
原因如下:
- 当前仓库绝大多数站点都以单文件 Spider 交付,新增公共基类会放大本次范围
- 参考实现已经清晰给出了接口路径、签名算法和字段映射,直接适配风险最低
- 当前需求重点是完整还原站点能力,而不是为未知后续站点提前抽象
## 模块边界
新增 `py/金牌.py`,只在模块内部维护站点逻辑,不修改 `base/`
对外实现以下接口:
- `init`
- `getName`
- `homeContent`
- `homeVideoContent`
- `categoryContent`
- `searchContent`
- `detailContent`
- `playerContent`
模块内部拆分以下 helper
- `_obj_to_form`
- 将请求参数按 `k=v&...` 拼接,忽略空值
- `_signed_headers`
- 根据请求参数、`APP_KEY` 和时间戳生成签名头
- `_fetch_json`
- 统一发起 GET 请求、校验状态码并解析 JSON
- `_map_vod`
- 统一列表项字段映射
- `_build_filters`
- 请求分类接口与筛选接口并组装首页筛选结构
- `_page_result`
- 统一分类和搜索的分页返回结构
- `_build_play_id`
- 将主视频 ID 与分集 ID 组合成短播放 ID
- `_parse_play_id`
- 解析 `主ID@子ID` 形式的播放 ID
## 站点配置与签名策略
固定配置来自参考实现:
- 站点名:`金牌`
- 主站:`https://m.jiabaide.cn`
- 默认 UA:移动端 Chrome UA
- `Referer``${host}/`
- `APP_KEY``cb808529bae6b6be45ecfab29a4889bc`
签名规则保持与参考实现一致:
1. 取当前毫秒时间戳作为 `t`
2. 将请求参数与 `key=APP_KEY``t` 合并
3.`k=v&...` 形式拼接
4. 先对拼接字符串做 `MD5`
5. 再对 `MD5` 结果做 `SHA1`
6.`t``sign` 放入请求头
请求策略:
- 接口请求统一使用 `GET`
- 参数通过 query string 传递
- 请求头统一带 `User-Agent``Referer``t``sign`
- 若响应状态码不是 `2xx`,或业务 `code` 不是成功值,则返回空结果而非抛出未处理异常
## 分类与首页设计
`homeContent` 负责同时返回分类、筛选和首页推荐。
分类来源:
- `/api/mw-movie/anonymous/get/filer/type`
筛选来源:
- `/api/mw-movie/anonymous/v1/get/filer/list`
首页推荐来源:
- `/api/mw-movie/anonymous/home/hotSearch`
筛选字段映射遵循参考实现:
- `typeList -> key=type, name=类型`
- `plotList -> key=class, name=剧情`
- `districtList -> key=area, name=地区`
- `languageList -> key=lang, name=语言`
- `yearList -> key=year, name=年份`
- `serialList -> key=by, name=排序`
排序选项固定为:
- 最近更新 -> `1`
- 添加时间 -> `2`
- 人气高低 -> `3`
- 评分高低 -> `4`
每个筛选项都默认插入“全部”。
`homeVideoContent` 保持仓库现有习惯,返回:
- `{"list": []}`
## 列表与搜索设计
分类接口使用:
- `/api/mw-movie/anonymous/video/list`
请求参数如下:
- `type1`
- 当前分类 ID
- `pageNum`
- 当前页码
- `pageSize`
- 固定 `30`
- `sort`
- 由筛选项 `by` 映射,默认 `1`
- `sortBy`
- 固定 `1`
- `type`
- 子类型筛选
- `v_class`
- 剧情筛选
- `area`
- 地区筛选
- `lang`
- 语言筛选
- `year`
- 年份筛选
搜索接口使用:
- `/api/mw-movie/anonymous/video/searchByWordPageable`
请求参数如下:
- `keyword`
- `pageNum`
- `pageSize`
返回结构遵循当前仓库约定:
- `page`
- `limit`
- `total`
- `list`
其中:
- `limit` 固定为 `30`
- 默认不返回 `pagecount`
- 空关键词搜索直接返回空列表
## 列表字段映射
列表页与搜索页的单项统一映射为:
- `vod_id`
- `vodId`
- `vod_name`
- `vodName`
- `vod_pic`
- `vodPic`
- `vod_remarks`
- `vodRemarks``vodDoubanScore``_` 连接,空值自动忽略
- `vod_year`
- 优先从 `vodPubdate` 提取年份
- `type_id`
- `typeId`
- `type_name`
- `typeName`
这样首页推荐、分类和搜索的列表结构保持一致。
## 详情设计
详情接口使用:
- `/api/mw-movie/anonymous/video/detail`
请求参数:
- `id`
详情返回只保留当前仓库需要的核心字段:
- `vod_id`
- `vod_name`
- `vod_pic`
- `type_name`
- `vod_remarks`
- `vod_year`
- `vod_area`
- `vod_lang`
- `vod_director`
- `vod_actor`
- `vod_content`
- `vod_play_from`
- `vod_play_url`
剧集列表来自 `episodeList`,每一集的播放 ID 设计为:
- `<vodId>@<nid>`
播放线路先收敛为单条:
- `金牌线路`
这样可以兼容仓库现有以 `vod_play_from` / `vod_play_url` 为核心的详情结构,同时避免把上游清晰度层级提前展开到详情页。
## 播放设计
播放接口使用:
- `/api/mw-movie/anonymous/v2/video/episode/url`
请求参数如下:
- `clientType=3`
- `id`
- 主视频 ID
- `nid`
- 分集 ID
播放页逻辑:
-`主ID@子ID` 解析出 `id``nid`
- 请求上游接口拿到 `list`
- 将上游返回的多清晰度地址整理为播放器可消费的结果
播放器返回结构:
- `parse: 0`
- `playUrl: ""`
- `url`
- 优先返回首个可用地址
- `header`
- 至少包含移动端 `User-Agent`
不额外引入 `pagecount``urls` 或其他参考 JS 的扩展字段,优先保持与当前 Python 仓库播放器返回习惯一致。
同时,为了兼容仓库中部分站点会保留扩展信息,模块内部仍会保留原始清晰度列表的整理逻辑;若调用方仅消费 `url`,则使用首个可用地址即可。
异常策略:
- 播放 ID 不是 `主ID@子ID` 格式时,返回空 URL
- 上游返回空列表时,返回空 URL
## 错误处理
所有公开接口都以“尽量返回空结果”作为失败策略:
- `homeContent`
- 失败时返回空分类、空筛选、空推荐
- `categoryContent`
- 失败时返回 `page/limit/total/list` 的空结构
- `searchContent`
- 失败时返回空分页结果
- `detailContent`
- 失败时返回 `{"list": []}`
- `playerContent`
- 失败时返回空 URL
不向上抛出未处理异常,保持与现有仓库 Spider 的容错风格一致。
## 测试设计
新增 `py/tests/test_金牌.py`,全部通过 mock 网络层验证离线行为。
至少覆盖以下测试:
1. `_signed_headers` 会写入 `t` 和正确的 `sign`
2. `homeContent` 能组合分类、筛选和首页推荐
3. `categoryContent` 能把 `by/class/area/lang/year/type` 正确映射到请求参数
4. `searchContent` 对空关键词直接返回空列表,对正常结果正确映射
5. `detailContent` 能输出基础元数据、单线路名与 `主ID@子ID` 剧集串
6. `playerContent` 能拆分播放 ID、请求上游并返回首个可用播放地址
7. 公开接口在上游异常或空数据时返回约定的空结构
测试原则:
- 不依赖真实网络
- 尽量验证最小行为单元
- 对签名和请求参数做精确断言,避免只测结果不测协议
## 风险与约束
主要风险如下:
- 上游匿名接口的业务 `code` 语义可能并不完全稳定,需要测试中明确覆盖成功与失败分支
- 参考 JS 的 `play` 返回了多清晰度数组,而仓库 Python Spider 通常以单个 `url` 为主,需要在实现中做兼容收敛
- 详情中的 `vodClass``vodYear``vodArea` 等字段可能出现缺失,需要统一空值兜底
对应约束如下:
- 优先保证仓库消费兼容性,而不是 1:1 复刻 JS 返回结构
- 不额外改动公共播放器约定
- 所有行为变化都必须由离线测试先定义再实现