From 3cc3543be73490c848779e3d58b6996006ce96ec Mon Sep 17 00:00:00 2001 From: Harold <8866033@gmail.com> Date: Mon, 20 Apr 2026 08:26:47 +0800 Subject: [PATCH] docs: add bubuyingshi spider design --- .../2026-04-20-bubuyingshi-spider-design.md | 369 ++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 py/docs/superpowers/specs/2026-04-20-bubuyingshi-spider-design.md diff --git a/py/docs/superpowers/specs/2026-04-20-bubuyingshi-spider-design.md b/py/docs/superpowers/specs/2026-04-20-bubuyingshi-spider-design.md new file mode 100644 index 0000000..b876650 --- /dev/null +++ b/py/docs/superpowers/specs/2026-04-20-bubuyingshi-spider-design.md @@ -0,0 +1,369 @@ +# 步步影视 Python 爬虫设计 + +## 目标 + +在当前 Python 仓库中新增一个符合 `base.spider.Spider` 接口的步步影视站点爬虫,覆盖以下能力: + +- 首页分类与筛选 +- 首页推荐视频 +- 分类列表 +- 搜索 +- 详情解析 +- 播放解析 + +实现基于站点现有 JSON API,不依赖浏览器自动化,不修改 `base/` 公共层。 + +## 范围 + +本次实现包含: + +- 新增独立脚本,文件名为 `步步影视.py` +- 使用单一站点主域:`https://bbys.app` +- 支持 `home/homeVideo/category/detail/search/player` 全链路 +- 支持类型、地区、年份、排序筛选 +- 支持首页接口失败时回退到主分类聚合 +- 支持播放线路解码 + +本次实现不包含: + +- 多域名自动探活与切换 +- 调试日志文件、路由注册和 Node 中间层封装 +- 浏览器执行、验证码处理或复杂反爬绕过 +- 登录态、会员内容和本地缓存持久化 +- 通用 JSON 影视站抽象层 + +## 方案选择 + +采用仓库现有的“单站点单文件 + 单测”方案: + +- 对外保持 `Spider` 接口兼容 +- 对内拆分为签名头生成、请求封装、列表映射、详情重组和播放解码几个 helper +- 保留参考 JS 的核心行为,但去掉 Fastify 路由层、调试日志和无关包装 + +不直接照搬参考 JS 路由层的原因是: + +- 当前仓库只消费 Spider 接口,不消费站内 HTTP API 路由 +- Python 版单文件 Spider 更符合现有项目结构 +- 测试重点应放在请求参数与返回映射,而不是路由分发 + +## 模块边界 + +新增脚本只在站点文件内部维护逻辑,不修改 `base/`。 + +脚本内部职责拆分如下: + +- `init` + - 初始化主域、固定头字段、分类、筛选定义和签名常量 +- `homeContent` + - 返回固定 `class` 与 `filters` +- `homeVideoContent` + - 请求首页接口并映射首页卡片,失败时回退到主分类聚合 +- `categoryContent` + - 请求分类接口并返回分页结果 +- `detailContent` + - 请求详情接口并整理影片元数据与播放列表 +- `searchContent` + - 请求搜索接口并映射结果列表 +- `playerContent` + - 解析压缩后的播放载荷,必要时调用解码接口 +- 私有辅助函数 + - 签名生成 + - `app/web` 请求头构造 + - 文本与数组归一化 + - 视频卡片映射 + - 首页数据映射 + - 播放线路重组 + - 播放地址解码与兜底 + +## Host 与请求策略 + +本次只实现单域: + +- `https://bbys.app` + +请求分两类: + +- `app` 接口:`/api.php/app/...` +- `web` 接口:`/api.php/web/...` + +固定签名参数来自参考实现: + +- `pkg = com.sunshine.tv` +- `ver = 4` +- `finger = SF-C3B2B41F6EFFFF9869176CF68F6790E8F07506FC88632C94B4F5F0430D5498CA` +- `sk = SK-thanks` +- `webSign = f65f3a83d6d9ad6f` +- `xClient = 8f3d2a1c7b6e5d4c9a0b1f2e3d4c5b6a` + +请求头策略: + +- `app` 请求带 `x-aid/x-time/x-sign/x-nonc/x-ave` +- `web` 请求在此基础上额外带 `web-sign/X-Client` +- 默认 `User-Agent` 维持参考实现的移动端接口风格 + +失败策略: + +- 单次请求失败返回空数据,不向上抛出未处理异常 +- 首页接口为空时回退到主分类聚合 +- 分类未知或为空时回退到主分类热门列表 + +不做: + +- 自动重试 +- 动态 host 探活 +- 写入本地日志文件 + +## 分类与筛选设计 + +首页分类固定为: + +- `1 -> 电影` +- `2 -> 剧集` +- `3 -> 动漫` +- `4 -> 综艺` + +筛选配置直接内置到脚本中,字段沿用参考实现: + +- `class` +- `area` +- `year` +- `by` + +年份值按当前年份动态生成: + +- 电影从当年递减到 2016,并补充区间项 +- 剧集从当年递减到 2021,并补充区间项 +- 动漫、综艺从当年递减到 2011,并补充“更早” + +`homeContent` 返回: + +- `class` +- `filters` + +`homeVideoContent` 不在 `homeContent` 内联返回,保持与仓库现有模式一致。 + +## 首页与列表设计 + +首页接口: + +- `GET /api.php/web/index/home` + +首页数据从 `data.categories` 提取: + +- 分类列表映射为 `type_id/type_name` +- 各分类下 `videos` 聚合后映射为统一卡片结构 + +卡片字段至少包含: + +- `vod_id` +- `vod_name` +- `vod_pic` +- `vod_remarks` +- `type_name` +- `vod_year` +- `vod_area` + +若首页接口为空或异常: + +- 依次请求“电影/剧集/综艺/动漫”四个主分类的热门列表 +- 聚合结果作为首页推荐 + +分类接口: + +- `GET /api.php/web/filter/vod` + +请求参数包括: + +- `type_name` +- `page` +- `sort` +- `class` +- `area` +- `year` + +分类返回字段: + +- `page` +- `limit` +- `total` +- `list` + +为了符合仓库约定,分类与搜索结果不返回 `pagecount`。 + +`limit/total` 采用保守策略: + +- 有结果时 `limit = len(list)` +- `total` 至少为当前页已知数量 +- 不承诺站点真实总页数 + +当外部传入未知分类值时: + +- 不直接报空 +- 自动回退到四个主分类热门列表聚合,减少壳子兼容性问题 + +## 搜索设计 + +搜索接口: + +- `GET /api.php/app/search/index` + +请求参数包括: + +- `wd` +- `page` +- `limit=15` + +返回结果统一映射为卡片结构: + +- `vod_id` +- `vod_name` +- `vod_pic` +- `vod_remarks` +- `type_name` +- `vod_year` +- `vod_area` + +空关键字直接返回空结果,不发起请求。 + +## 详情页设计 + +详情接口: + +- `GET /api.php/web/vod/get_detail?vod_id=` + +接口可能返回对象或数组,设计上统一兼容: + +- 若为数组则取首项 +- 若为空则返回空 `list` + +详情输出字段至少包含: + +- `vod_id` +- `vod_name` +- `vod_pic` +- `vod_remarks` +- `vod_year` +- `vod_area` +- `vod_actor` +- `vod_director` +- `vod_content` +- `type_name` +- `vod_play_from` +- `vod_play_url` + +说明: + +- `vod_content` 需移除 HTML 标签并把段落、换行转为纯文本 +- `type_name` 优先取站点详情中的分类字段 + +### 播放线路重组 + +站点详情返回的原始播放字段: + +- `vod_play_from` +- `vod_play_url` + +两者都可能以 `$$$` 分组,以 `#` 分集,以 `$` 分割标题和地址。 + +实现时将其重组为仓库侧可用格式: + +- 每个线路名显示为 `线路名(集数)` +- 每个分集地址编码为 `标题$线路名@1@原始地址` + +规则: + +- 空线路组跳过 +- 分集无标题时兜底为“播放” +- 没有线路名时兜底为 `lineN` + +## 播放设计 + +播放阶段优先消费详情中编码后的 `线路名@1@原始地址`。 + +### 输入解析 + +支持三类输入: + +- `线路名@1@原始地址` +- 直接 `http/https` 地址 +- 未带前缀的原始地址 + +解析规则: + +- `@1@` 表示需要调用解码接口 +- 已经是绝对地址时直接透传 +- 未带协议且未带前缀时视为需要解码 + +### 解码接口 + +- `GET /api.php/app/decode/url/?url=&vodFrom=` + +返回结构可能是: + +- `data` 为字符串 +- `data.url` +- `url` + +实现时依次兼容提取。 + +### 输出规则 + +播放器结果字段: + +- `parse = 0` +- `playUrl = ""` +- `url = 最终地址` + +当最终地址属于以下站点时,额外标记需要嗅探: + +- `iqiyi.com` +- `v.qq.com` +- `youku.com` +- `mgtv.com` +- `bilibili.com` + +兼容策略: + +- 仓库现有 Spider 接口不统一消费 `jx` +- 本次实现仍保留 `jx` 字段,便于上层兼容支持 + +解码失败时: + +- 回退返回原始输入地址 +- 不抛出异常 + +## 测试设计 + +新增 `tests/test_步步影视.py`,使用 `unittest` 和 `unittest.mock`,不访问真实网络。 + +首批测试覆盖: + +- 签名头包含必要字段,`web` 头附带 `web-sign/X-Client` +- `homeContent` 返回固定分类与筛选 +- `homeVideoContent` 可从首页接口提取分类和视频,并在失败时走主分类聚合 +- `categoryContent` 正确拼接查询参数并映射列表 +- 未知分类触发主分类聚合回退 +- `searchContent` 对空关键字不发请求,对有效关键字正确构造参数 +- `detailContent` 兼容对象和数组详情,能完成 HTML 简介清洗 +- 播放线路重组能处理多线路、多分集和空标题兜底 +- `playerContent` 能解析编码播放 ID,并在需要时调用解码接口 +- 解码结果兼容 `data/data.url/url` 三种结构 + +验证顺序: + +- 先跑 `tests/test_步步影视.py` +- 通过后再视情况补跑更大范围测试 + +## 风险与取舍 + +主要风险: + +- 站点接口签名字段未来可能变更 +- 首页与分类接口的真实响应结构可能出现字段漂移 +- 解码接口返回格式不稳定 + +本次取舍: + +- 优先保证接口兼容和测试可维护性 +- 不为未知响应结构做过度抽象 +- 不引入真实联网测试,避免把站点波动带进单测