From 2aed7f5bc3711d0e247758e6db7d95522d8b30b4 Mon Sep 17 00:00:00 2001 From: Harold <8866033@gmail.com> Date: Fri, 24 Apr 2026 17:33:58 +0800 Subject: [PATCH] docs: add dyrs spider design --- .../specs/2026-04-24-dyrs-spider-design.md | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 py/docs/superpowers/specs/2026-04-24-dyrs-spider-design.md diff --git a/py/docs/superpowers/specs/2026-04-24-dyrs-spider-design.md b/py/docs/superpowers/specs/2026-04-24-dyrs-spider-design.md new file mode 100644 index 0000000..09034a4 --- /dev/null +++ b/py/docs/superpowers/specs/2026-04-24-dyrs-spider-design.md @@ -0,0 +1,172 @@ +# 电影人生 Spider 设计 + +## 目标 + +在当前 Python 仓库中新增一个符合 `base.spider.Spider` 接口的 `电影人生` spider,参考用户提供的 JS 站点逻辑,实现首页、分类、搜索、详情和播放能力,目标站点为 `https://dyrsok.com`。 + +## 范围 + +本次实现包含: + +- 新增 `py/电影人生.py` +- 新增 `py/tests/test_电影人生.py` +- 固定 4 个主分类与静态筛选项 +- 首页推荐、分类页与搜索页卡片解析 +- 详情页元数据与多线路剧集解析 +- 播放页 `/api/m3u8` 提取、重定向跟随和主清单二次解析 + +本次实现不包含: + +- 修改 `py/base/` 共享基类 +- 接入 JS 版本中的 `OmniBox.log`、播放历史、媒体信息探测 +- 真实联网测试 +- 站外解析器或额外第三方线路适配 + +## 分类与筛选 + +固定返回以下分类: + +- `dianying` 电影 +- `dianshiju` 电视剧 +- `dongman` 动漫 +- `zongyi` 综艺 + +筛选结构采用静态定义,保持与参考脚本一致的语义: + +- `class` + - 不同主分类下返回预设类型值 +- `sort_field` + - 默认、热度、更新时间 + +Python 版本沿用仓库现有格式,即筛选值使用 `{"n": "...", "v": "..."}`。 + +## 接口映射 + +- `homeContent` + - 返回固定分类和筛选配置 + - 可附带首页推荐列表 +- `homeVideoContent` + - 请求首页 + - 复用卡片解析函数返回推荐列表 +- `categoryContent` + - 构造 `/{categoryId}.html`,并按筛选拼接查询参数 + - 解析卡片并返回分页字段 +- `detailContent` + - 请求详情页 + - 解析标题、封面、简介、年份、地区、语言、导演、主演、标签 + - 解析多线路及每条线路下的剧集 +- `searchContent` + - 构造 `/s.html?name=` + - 解析卡片并执行结果精排 +- `playerContent` + - 输入为详情页剧集项里编码的播放上下文 + - 请求播放页并提取 `/api/m3u8?origin=...&url=...` + - 跟随 302 或 Location 跳转 + - 若主清单中存在 `raw=1` 子清单,则继续下钻到最终可播 m3u8 + - 失败时回退为 `parse=1` + +## 数据模型 + +### 列表卡片 + +列表、首页推荐和搜索统一复用卡片解析逻辑: + +- 从 `a[data-url][title]` 提取标题和详情链接 +- 仅保留命中 `/dyrscom-` 的详情链接 +- 图片优先取 `data-src`,其次 `src` +- 备注优先取角标文本 +- 补全相对链接与相对图片地址 + +`vod_id` 保留站内短路径,不返回绝对 URL。 + +### 详情 + +详情页字段按“DOM 优先、正则兜底”的策略提取: + +- 标题优先 `h1/h2`,再回退到 `title` +- 封面优先 `og:image`,再回退到详情主图 +- 简介优先“剧情简介”区域,回退到 `description` +- 年份、地区、语言、更新时间通过标签文本或正则抽取 +- 导演、主演、标签通过各自区块中的链接文本抽取 + +播放线路通过详情页上的 `origin` 链接展开: + +- 收集 `originTabs` 中的线路入口 +- 每条线路单独抓取页面 +- 在 `.seqlist` 中提取剧集 +- 剧集 ID 使用 JSON 串编码,至少携带: + - `title` + - `origin` + - `page` + - `vodName` + - `pic` + +最终输出为仓库当前通用格式: + +- `vod_play_from`: `$$$` 拼接线路名 +- `vod_play_url`: `$$$` 拼接每条线路的 `剧集名$playId` + +## 搜索精排 + +搜索结果在卡片解析后再做一次轻量精排,以贴近参考脚本行为: + +- 先标准化关键词和片名 +- 去除空白、标点与部分年份噪音 +- 完全匹配优先 +- 前缀匹配次之 +- 包含匹配再次之 +- 若所有项都未命中,则回退原始顺序 + +## 播放解析 + +播放解析只实现站内主链,不把 JS 版本中的外围运行时逻辑迁移进来。 + +处理顺序如下: + +1. 解析传入的 `playId` + - 若是 JSON,读取 `page/title/origin/vodName/pic` + - 若不是 JSON,则当作原始播放页地址 +2. 请求播放页 HTML +3. 用正则提取 `/api/m3u8?origin=...&url=...` +4. 请求该接口并检查响应头里的 `Location` + - 有跳转时以跳转后的地址为候选结果 +5. 若跳转结果是 m3u8 主清单,则读取文本 + - 若存在 `/api/m3u8?id=...raw=1` 行,则拼成绝对地址并作为最终结果 +6. 若任一步失败,则返回 `parse=1`,由外部继续解析 + +播放器返回头至少包含: + +- `User-Agent` +- `Referer` +- 必要时补 `Origin` + +## 错误处理 + +- HTML 请求失败时返回空字符串,不向上抛站点异常 +- 详情页解析失败时返回空列表 +- 分类、搜索失败时返回空列表和基本分页字段 +- 播放解析失败时回退到播放页 URL 或空地址,不报未处理异常 +- 所有 helper 对缺失字段、坏 JSON、空节点、无匹配正则保持容错 + +## 测试策略 + +使用 `unittest` 与 `unittest.mock`,不走真网。 + +至少覆盖: + +- `homeContent` 返回 4 个分类和对应 filters +- 卡片解析正确提取 `vod_id`、标题、图片、备注 +- `categoryContent` 正确拼装 URL 与查询参数 +- `detailContent` 正确提取元数据并组装多线路剧集 +- `searchContent` 调用搜索 URL 并执行精排 +- `playerContent` 成功提取 `/api/m3u8` +- `playerContent` 能处理 302 跳转 +- `playerContent` 能从主清单中继续提取 `raw=1` 子清单 +- 无法提取直链时回退 `parse=1` + +## 验收标准 + +- 新增 spider 与测试文件后,可独立运行 `tests.test_电影人生` +- 首页、分类、搜索、详情在离线夹具下输出稳定 +- 播放支持“接口提取 -> 跳转 -> 主清单下钻 -> 失败回退”四条路径 +- 不修改共享基类即可完成接入