From 5e49b8f4fa25f8ea43579af72f1e16b6daf76ed5 Mon Sep 17 00:00:00 2001 From: Harold <8866033@gmail.com> Date: Fri, 24 Apr 2026 18:08:07 +0800 Subject: [PATCH] docs: add ruyi spider design spec --- .../specs/2026-04-24-ruyi-spider-design.md | 329 ++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 py/docs/superpowers/specs/2026-04-24-ruyi-spider-design.md diff --git a/py/docs/superpowers/specs/2026-04-24-ruyi-spider-design.md b/py/docs/superpowers/specs/2026-04-24-ruyi-spider-design.md new file mode 100644 index 0000000..d8821a9 --- /dev/null +++ b/py/docs/superpowers/specs/2026-04-24-ruyi-spider-design.md @@ -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`、`null` 或空值时返回空字符串 +- 图片字段为相对路径时,拼接第一个图片主机 + +## 详情设计 + +详情接口使用: + +- `ac=videolist` +- `ids=` + +详情返回只保留当前仓库需要的核心字段: + +- `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 约定 +- 不引入刮削、弹幕、嗅探等额外行为