docs: add jinpai spider design

This commit is contained in:
Harold
2026-04-24 16:23:34 +08:00
parent d7aa38154e
commit 153589549d
@@ -0,0 +1,342 @@
# 金牌 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 返回结构
- 不额外改动公共播放器约定
- 所有行为变化都必须由离线测试先定义再实现