docs: add ruyi spider design spec

This commit is contained in:
Harold
2026-04-24 18:08:07 +08:00
parent c82b47f4f2
commit 5e49b8f4fa
@@ -0,0 +1,329 @@
# 如意资源 Python 爬虫设计
## 目标
在当前 Python 仓库中新增一个符合 `base.spider.Spider` 接口的如意资源爬虫,行为参考用户提供的 JS 版本,但范围只保留基础采集能力:
- 首页分类与筛选
- 首页推荐
- 分类浏览
- 搜索
- 详情解析
- 播放解析
实现形式遵循当前仓库的单文件 Spider 约定和离线单测约定,不引入 OmniBox 专有能力,也不修改公共基类。
## 范围
本次实现包含:
- 新增独立脚本 `py/如意资源.py`
- 新增独立测试 `py/tests/test_如意资源.py`
- 顺序尝试多个采集 API,直到拿到可用 JSON
- 使用硬编码主分类、子分类和筛选项
- 首页返回分类、筛选和推荐列表
- 分类支持类型筛选和分页
- 搜索支持分页与空关键词保护
- 详情组装基础元数据与多线路播放列表
- 播放接口区分直链与待解析链接
本次实现不包含:
- 参考 JS 中的刮削重命名
- 弹幕匹配
- 嗅探能力
- 播放页探测与二次解析页面构造
- 修改 `base/` 公共层
- 真实联网集成测试
## 方案选择
采用“仓库风格重写”的方案,而不是直接把 JS 逻辑逐行搬到 Python。
原因如下:
- 当前仓库是 Python 单文件 Spider 模式,直接适配更一致
- 用户已明确只要基础采集,没必要保留 OmniBox 的包装层和状态管理
- 仓库测试习惯更适合覆盖字段映射、fallback 和播放分支,而不是复制 JS 中的运行时行为
## 模块边界
新增 `py/如意资源.py`,只在模块内部维护站点逻辑,不修改 `base/`
对外实现以下接口:
- `init`
- `getName`
- `homeContent`
- `homeVideoContent`
- `categoryContent`
- `searchContent`
- `detailContent`
- `playerContent`
模块内部拆分以下 helper
- `_request_json`
- 统一对多个 API 执行 GET 请求和 fallback
- `_build_query`
- 拼接 query string,忽略空值
- `_get_pic_url`
- 统一补全图片 URL
- `_format_vod_list`
- 将列表接口数据映射为仓库统一字段
- `_page_result`
- 统一列表/搜索分页返回结构
- `_resolve_type_id`
- 根据主类和筛选结果推导最终请求的类型 ID
- `_parse_play_groups`
- 把详情接口返回的播放来源和剧集字符串组装成 `vod_play_from` / `vod_play_url`
- `_is_direct_media_url`
- 判断播放链接是否为明显的直链媒体地址
## 站点配置
固定配置来自参考实现:
- 站点名:`如意资源`
- API 列表:
- `https://cj.rycjapi.com/api.php/provide/vod`
- `https://cj.rytvapi.com/api.php/provide/vod`
- `https://bycj.rytvapi.com/api.php/provide/vod`
- 图片主机回退:
- `https://ps.ryzypics.com`
- `https://ry-pic.com`
- `https://img.lzzyimg.com`
- 默认请求头:
- `User-Agent`
- `Accept: application/json`
- `Accept-Language: zh-CN,zh;q=0.9`
- `Referer: https://cj.rycjapi.com/`
- 请求超时:
- `10`
请求策略:
- 所有采集接口统一使用 `GET`
- 若显式传入完整 URL,则直接请求该 URL
- 否则按 API 列表顺序发起请求
- 单个 API 非 `200`、JSON 解析失败或业务字段异常时,继续尝试下一个 API
- 所有 API 均失败时返回空结果而不是抛出未处理异常
## 分类与筛选设计
分类和筛选直接采用硬编码配置,避免依赖站点分类接口的不稳定性。
主分类固定为:
- `1` 电影片
- `2` 连续剧
- `3` 综艺片
- `4` 动漫片
- `35` 电影解说
- `36` 体育
子分类映射沿用参考实现,例如:
- 电影片默认落到 `7`
- 连续剧默认落到 `13`
- 综艺片默认落到 `25`
- 动漫片默认落到 `29`
- 体育默认落到 `37`
`homeContent` 返回:
- `class`
- `filters`
同时补充首页推荐列表,便于与仓库中支持推荐的蜘蛛保持一致。
筛选结构遵循当前仓库常见格式:
- 每个主分类下只暴露一个 `key=type`
- `name=类型`
- `value` 为该主分类可选的硬编码子分类
无子分类的主类不返回筛选项。
`homeVideoContent` 保持仓库现有习惯,返回:
- `{"list": []}`
## 首页、分类与搜索设计
首页推荐接口使用:
- `ac=list`
- `pg=1`
- `pagesize=20`
分类接口使用:
- `ac=list`
- `t=<最终类型ID>`
- `pg=<页码>`
- `pagesize=20`
最终类型 ID 的推导规则:
1.`extend.type` 存在且非空,则直接使用该值
2. 否则若主分类存在子分类映射,则使用该主类的第一个子分类
3. 否则使用主分类 ID 本身
搜索接口使用:
- `ac=list`
- `wd=<关键词>`
- `pg=<页码>`
- `pagesize=30`
搜索结果保留接口返回的列表映射,但会额外按标题包含关键词做一次本地过滤,尽量贴近参考实现。
返回结构遵循当前仓库约定:
- `page`
- `limit`
- `total`
- `list`
其中:
- 首页推荐只返回 `list`
- 分类的 `limit` 固定为 `20`
- 搜索的 `limit` 固定为 `30`
- 默认不返回 `pagecount`
## 列表字段映射
首页推荐、分类和搜索统一映射为:
- `vod_id`
- `vod_id`
- `vod_name`
- `vod_name`
- `vod_pic`
- 优先使用 `vod_pic`,必要时补全为完整 URL
- `vod_remarks`
- 优先使用 `vod_remarks`,否则退回年份
- `vod_year`
- `vod_year`
- `type_id`
- `type_id`
数据清洗规则:
- 过滤空对象、空 `vod_id``vod_id=0`
- `vod_name` 为空时回退为“未知标题”
- 图片字段为 `<nil>``nil``null` 或空值时返回空字符串
- 图片字段为相对路径时,拼接第一个图片主机
## 详情设计
详情接口使用:
- `ac=videolist`
- `ids=<vod_id>`
详情返回只保留当前仓库需要的核心字段:
- `vod_id`
- `vod_name`
- `vod_pic`
- `type_name`
- `vod_year`
- `vod_area`
- `vod_remarks`
- `vod_actor`
- `vod_director`
- `vod_content`
- `vod_play_from`
- `vod_play_url`
播放列表解析规则:
-`vod_play_from` 读取线路名,逗号分隔
-`vod_play_url` 读取分集,`#` 分隔
- 每个分集项按 `标题$地址` 解析
- 若分集缺少标题,则回退为 `第N集`
- 若缺少地址,则跳过
为了兼容仓库现有数据结构,详情输出不保留 JS 版的嵌套 `vod_play_sources`,而是直接组装为:
- `vod_play_from`: 用 `$$$` 连接的线路名
- `vod_play_url`: 与线路一一对应、用 `$$$` 分组、每组内部用 `#` 连接的剧集串
若参考站点只返回一组剧集但含多个线路名,则默认把同一组剧集复制到每个线路名下,以保持与参考实现的可播放行为一致。
## 播放设计
播放阶段只做基础采集,不做嗅探和刮削增强。
输入为详情里拼出的剧集地址。处理规则如下:
1. 若地址为空,返回空播放结果
2. 若地址是明显的媒体直链,返回:
- `parse=0`
- `playUrl=""`
- `url=<直链>`
3. 若地址是普通 `http/https` 链接但不是明显媒体直链,返回:
- `parse=1`
- `playUrl=""`
- `url=<原始链接>`
4. 若地址不是 URL,仍按待解析地址返回,交给上游处理
明显媒体直链的判定后缀包括:
- `.m3u8`
- `.mp4`
- `.flv`
- `.avi`
- `.mkv`
- `.ts`
本次实现不为播放结果附加自定义 header,除非测试或站点字段证明有明确必需值。
## 错误处理
错误处理原则是“接口尽量返回空结果,不向外抛异常”。
具体策略:
- 请求失败时继续 fallback
- 分类、首页、搜索失败时返回空列表
- 详情失败时返回 `{"list": []}`
- 播放失败时返回 `{"parse": 0, "playUrl": "", "url": "", "header": {}}`
- 局部字段缺失只影响对应字段,不使整条记录失效
## 测试设计
测试文件新增为 `py/tests/test_如意资源.py`,全部使用 `unittest``mock`,不依赖真实网络。
首轮测试覆盖:
- `homeContent` 暴露硬编码分类和筛选
- `_get_pic_url` 处理绝对路径、相对路径和空值
- `_request_json` 在首个 API 失败时会 fallback 到下一个 API
- `homeContent` 或首页推荐正确映射列表字段
- `categoryContent` 能从主类推导默认子类,也能接受 `extend.type`
- `searchContent` 对空关键词直接返回空结果
- `searchContent` 正确传递搜索参数并做标题过滤
- `detailContent` 正确映射基础字段并拼出 `vod_play_from` / `vod_play_url`
- `playerContent` 对直链返回 `parse=0`
- `playerContent` 对非直链 URL 返回 `parse=1`
测试遵循 TDD
1. 先写失败测试
2. 确认失败原因正确
3. 再补最小实现
4. 最后跑目标模块测试
## 验收标准
完成后应满足以下标准:
- 新蜘蛛文件和对应测试文件均已存在
- 所有新增测试离线可运行
- 分类、推荐、分类列表、搜索、详情、播放六类基础能力可用
- 返回结构符合当前仓库 Spider 约定
- 不引入刮削、弹幕、嗅探等额外行为