docs: add wooyun spider design spec

This commit is contained in:
Harold
2026-04-19 13:08:32 +08:00
parent 80916ae626
commit 6b3bc6520f
@@ -0,0 +1,311 @@
# 乌云影视 Python 爬虫设计
## 目标
在当前 Python 仓库中新增一个符合 `base.spider.Spider` 接口的乌云影视站点爬虫,覆盖以下能力:
- 首页分类与筛选
- 首页推荐视频
- 分类列表
- 搜索
- 详情解析
- 播放解析
实现基于站点现有 JSON API,不依赖浏览器自动化,不修改 `base/` 公共层。
## 范围
本次实现包含:
- 新增独立脚本,文件名为 `乌云影视.py`
- 使用单一站点主域:`https://wooyun.tv`
- 支持 `home/homeVideo/category/detail/search/player` 全链路
- 支持年份、地区、类型、语言、排序筛选
- 支持多季播放列表
- 支持播放接口直链优先与播放回退
本次实现不包含:
- 多域名自动回退
- 登录态或会员内容适配
- 站点反爬绕过
- 通用 JSON 影视站抽象层
- 参考 JS 的 Fastify 路由封装
## 方案选择
采用仓库现有的“单站点单文件 + 单测”方案:
- 对外保持 `Spider` 接口兼容
- 对内拆分为请求封装、分类过滤构造、列表映射、详情解析、播放载荷编码几个 helper
- 保留参考实现的核心行为,但去掉服务端路由与日志包装层
不直接照搬参考 JS 包装层的原因是:
- 当前仓库只需要站点 Spider,不需要 HTTP 路由注册
- Python 版单文件 Spider 更符合现有项目结构
- 测试重点应放在参数构造和解析结果,而不是中间层框架逻辑
## 模块边界
新增脚本只在站点文件内部维护逻辑,不修改 `base/`
脚本内部职责拆分如下:
- `init`
- 初始化主域、请求头、默认分类、默认分页参数
- `homeContent`
- 拉取菜单接口,构造 `class``filters`
- `homeVideoContent`
- 拉取首页推荐接口,返回首页卡片列表
- `categoryContent`
- 组装搜索接口请求体,返回分页列表
- `detailContent`
- 组合基础详情、扩展详情和剧集列表,整理成 TVBox 详情结构
- `searchContent`
- 复用搜索接口执行关键字查询
- `playerContent`
- 解析编码后的播放载荷,优先返回直链,必要时回查剧集接口
- 私有辅助函数
- 安全文本转换
- 相对地址补全
- 筛选数据归一化
- 分类与筛选构造
- 卡片映射
- 播放载荷编码与解码
- 兜底剧集地址查找
## Host 与请求策略
本次只实现单域:
- `https://wooyun.tv`
请求统一走站点 JSON API,固定请求头至少包含:
- `Accept`
- `Origin`
- `Referer`
- `User-Agent`
其中:
- GET/POST 都走 `self.fetch` / `self.post`
- 默认超时使用站点级固定值,优先取 10 秒到 15 秒
- 非 2xx 响应视为失败并回退为空结果或播放解析
不做:
- 自动重试
- 动态 host 探活
- 本地缓存持久化
## 分类与筛选设计
菜单接口:
- `/movie/category/menu`
分类优先保留以下根分类:
- `movie -> 电影`
- `tv_series -> 电视剧`
- `animation -> 动画`
- `variety -> 综艺`
- `short_drama -> 短剧`
同时补充特殊年份分类:
- `THIS_YEAR -> 今年`
- `LAST_YEAR -> 去年`
筛选定义从菜单接口动态生成,按站点字段映射为:
- `year -> 年份`
- `region -> 地区`
- `genre -> 类型`
- `lang -> 语言`
- `sort -> 排序`
排序候选固定为:
- `default`
- `latest`
- `hot`
- `score`
`homeContent` 返回:
- `class`
- `filters`
分类接口请求体使用:
- `menuCodeList`
- `pageIndex`
- `pageSize`
- `searchKey`
- `sortCode`
- `topCode`
其中 `THIS_YEAR``LAST_YEAR` 不作为 `topCode`,只作为 `menuCodeList` 过滤项,`topCode` 仍回落到 `movie`
## 首页与列表设计
首页推荐接口:
- `/movie/media/home/custom/classify/1/3?limit=<n>`
推荐列表从区块数据中提取 `mediaResources`,按 `id` 去重后映射为统一卡片结构:
- `vod_id`
- `vod_name`
- `vod_pic`
- `type_id`
- `type_name`
- `vod_remarks`
- `vod_year`
- `vod_douban_score`
- `vod_actor`
- `vod_director`
列表与搜索都复用相同映射逻辑,封面优先级为:
- `posterUrlS3`
- `posterUrl`
- `thumbUrlS3`
- `thumbUrl`
分类接口:
- `POST /movie/media/search`
分页字段返回:
- `page`
- `pagecount`
- `limit`
- `total`
- `list`
其中 `limit` 固定为 24,与参考实现保持一致。
## 详情页设计
详情阶段使用三个接口并行组合:
- `/movie/media/base/detail?mediaId=<id>`
- `/movie/media/detail?mediaId=<id>`
- `/movie/media/video/list?mediaId=<id>&lineName=&resolutionCode=`
详情合并规则:
- 优先使用扩展详情接口字段
- 基础详情作为兜底
- 剧集列表单独解析播放源
输出字段至少包含:
- `vod_id`
- `vod_name`
- `vod_pic`
- `type_id`
- `type_name`
- `vod_remarks`
- `vod_year`
- `vod_area`
- `vod_actor`
- `vod_director`
- `vod_content`
- `vod_douban_score`
- `vod_play_from`
- `vod_play_url`
多季播放列表按季拆分:
- 多季时 `vod_play_from``第1季$$$第2季...`
- 单季时 `vod_play_from` 使用线路名或站点名兜底
每个剧集条目名称优先按 `第N集` 生成,存在备注时拼接为 `第N集 <remark>`,无法取到集数时回退 `正片`
## 播放设计
播放列表中的每一集不直接暴露站点原始对象,而是把最小必要字段编码为可逆载荷:
- `mediaId`
- `seasonNo`
- `epNo`
- `videoId`
- `playUrl`
- `name`
编码格式为 base64url JSON 字符串。
`playerContent` 处理流程:
1. 解码播放载荷
2. 若载荷内已有 `playUrl`,直接返回补全后的绝对地址
3. 若没有 `playUrl` 但有 `mediaId`,回查剧集接口并按 `seasonNo/videoId/epNo` 找到目标地址
4. 若仍拿不到地址,回退为 `parse=1` 并把原始 `id` 交给外层解析
直链返回头只保留播放必要字段:
- `Referer`
- `Origin`
- `User-Agent`
## 搜索设计
搜索复用:
- `POST /movie/media/search`
请求体固定:
- `menuCodeList = []`
- `pageIndex = pg`
- `pageSize = 24`
- `searchKey = keyword`
- `sortCode = ""`
- `topCode = ""`
空关键字直接返回空列表,不发请求。
## 错误处理
核心策略保持简单明确:
- HTTP 非 2xx 直接抛错
- JSON 结构中若存在站点失败标记,也抛错
- `home/homeVideo/category/search/detail` 失败时返回空结构或上层调用感知异常
- `playerContent` 失败时优先回退到 `parse=1`
不增加复杂的错误码映射,只保留对调用方有意义的最小错误信息。
## 测试设计
测试文件:
- `tests/test_乌云影视.py`
单测覆盖以下边界:
- 菜单数据能够构造固定分类与动态筛选
- 首页推荐区块能去重并映射统一卡片结构
- 分类请求体能正确处理根分类与年份特殊分类
- 分类结果能返回正确分页字段
- 详情页能合并详情字段并生成多季播放列表
- 播放载荷编码解码可逆
- 播放回查逻辑能按 `videoId``epNo` 命中剧集地址
- 搜索空关键字直接返回空结构
- 播放在没有直链时回退 `parse=1`
测试以 mock HTTP 返回和纯函数解析为主,不依赖真实网络。
## 实现约束
- 不修改 `base/spider.py`
- 不引入新第三方依赖
- 维持仓库现有返回结构命名
- 代码以 ASCII 为主,中文仅用于站点名和用户可见字段
- 与现有站点脚本风格保持一致,优先单文件内聚实现