diff --git a/py/docs/superpowers/specs/2026-04-29-shuangxing-spider-design.md b/py/docs/superpowers/specs/2026-04-29-shuangxing-spider-design.md new file mode 100644 index 0000000..55b584e --- /dev/null +++ b/py/docs/superpowers/specs/2026-04-29-shuangxing-spider-design.md @@ -0,0 +1,446 @@ +# 双星 Python 爬虫设计 + +**日期:** 2026-04-29 + +## 目标 + +在当前 Python Spider 仓库中新增独立单站蜘蛛 `双星.py`。 + +目标站点主域名固定为: + +- `https://1.star2.cn` + +行为边界以用户提供的 Python 参考实现为准,并对齐当前仓库蜘蛛接口: + +- 支持固定分类输出 +- 支持分类分页列表 +- 支持关键字搜索 +- 支持详情页标题和网盘链接整理 +- `playerContent` 只透传已识别的网盘分享链接 +- 不做站内播放页解析 + +## 范围 + +本次实现包含: + +- 新增蜘蛛文件 `py/双星.py` +- 新增测试文件 `py/tests/test_双星.py` +- 补充对应 spec 和 plan 文档 + +本次实现不包含: + +- 抽取新的公共盘站基类 +- 修改 `base/` 公共层 +- 增加站内直链解析 +- 接入外部聚合蜘蛛 +- 实现多备用域名 failover + +## 现状 + +仓库中已经存在多份结构接近的网盘聚合站蜘蛛,尤其是: + +- `py/欧歌.py` +- `py/闪电.py` +- `py/二小.py` + +这些实现已经验证了当前仓库对同类站点的稳定接入方式: + +- `homeContent/homeVideoContent/categoryContent/searchContent/detailContent/playerContent` 作为统一入口 +- `vod_id` 保持为站内短路径 +- `detailContent` 只负责把详情页里的网盘分享链接整理成播放线路 +- `playerContent` 不做二次路由,直接对支持的网盘链接透传 + +双星和上述盘站不同的一点是: + +- 首次访问首页后需要缓存响应 cookie,后续列表、搜索和详情请求都要带回该 cookie + +因此,本次工作的重点是: + +- 在现有盘站模式下补上 cookie 初始化链路 +- 按双星页面结构实现列表、搜索和详情解析 +- 保持 `playerContent` 返回形状与仓库现有蜘蛛兼容 + +## 方案选择 + +采用“独立单站实现 + 仓库接口适配”的方案。 + +### 方案 A:推荐 + +- 新增 `py/双星.py` +- 参考用户给出的 Python 逻辑实现 cookie 初始化和 DOM 解析 +- 对外接口对齐仓库现有 `Spider` 约定 +- 使用 `unittest` 做离线单测 + +优点: + +- 与当前仓库现有调用链兼容 +- 风险边界小 +- 测试可直接复用已有盘站蜘蛛的断言模式 + +### 方案 B:不采用 + +- 严格照搬用户参考中的 `route/id` 分发模式 + +不采用原因: + +- 与当前仓库 `playerContent` 返回结构不一致 +- 会在单站蜘蛛里引入额外的路由语义 +- 对当前调用方没有直接收益 + +## Spider 对外行为 + +### 文件 + +- `py/双星.py` +- `py/tests/test_双星.py` + +### `init` + +初始化时访问首页: + +- URL:`https://1.star2.cn` +- 请求头至少包含 `User-Agent` 和 `Referer` +- `allow_redirects=False` + +从首页响应 cookies 中提取 `name=value`,按 `; ` 拼接后缓存到实例字段中。 + +设计约束: + +- `init` 失败不做复杂重试 +- 后续请求统一通过 helper 带上缓存 cookie +- 如果 cookie 为空,后续请求仍然允许继续发送基础请求头 + +### `homeContent` + +返回固定 7 个分类: + +- `ju -> 国剧` +- `zy -> 综艺` +- `mv -> 电影` +- `rh -> 日韩` +- `ym -> 英美` +- `wj -> 外剧` +- `dm -> 动漫` + +返回结构: + +```python +{"class": [...]} +``` + +不返回筛选项。 + +### `homeVideoContent` + +返回: + +```python +{"list": []} +``` + +### `categoryContent` + +分类 URL: + +- `/{cate_id}_{page}/` + +例如: + +- `/ju_1/` + +解析容器: + +- `body > div > div > main > div > ul > li` + +每个列表项提取: + +- `vod_id`:`div.a > a[href]` +- `vod_name`:`div.a > a` 文本 +- `vod_pic`:固定空字符串 +- `vod_remarks`:固定空字符串 + +结果结构包含: + +- `page` +- `limit` +- `total` +- `list` + +不返回 `pagecount`。 + +`limit` 采用站点参考中的每页基准值 `15`,`total` 采用仓库当前惯例的保守估算: + +- `total = (page - 1) * 15 + len(list)` + +这样可以避免伪造不可验证的总页数。 + +### `searchContent` + +搜索 URL: + +- `/search/?keyword={quote(keyword)}&page={page}` + +空关键词直接返回: + +```python +{"page": page, "total": 0, "list": []} +``` + +解析容器与字段同分类页: + +- `body > div > div > main > div > ul > li` +- `vod_id`:`div.a > a[href]` +- `vod_name`:`div.a > a` 文本 +- `vod_pic`:空字符串 +- `vod_remarks`:空字符串 + +返回结构包含: + +- `page` +- `total` +- `list` + +### `detailContent` + +输入 `vod_id` 为站内相对路径,例如: + +- `/post/123` + +内部请求时用 `urljoin` 拼成完整详情 URL。 + +详情页提取: + +- 标题:`body > div > div.s20erx.erx-m-bot.erx-content > main > article > h1` +- 分享链接:`#maximg > div.dlipp-cont-wp > div > div.dlipp-cont-bd > a[href]` + +输出字段: + +- `vod_id` +- `vod_name` +- `vod_pic` +- `vod_remarks` +- `vod_content` +- `vod_director` +- `vod_actor` +- `vod_play_from` +- `vod_play_url` + +其中: + +- `vod_pic` 固定空字符串 +- `vod_remarks` 固定空字符串 +- `vod_content` 固定空字符串 +- `vod_director` 固定空字符串 +- `vod_actor` 固定空字符串 + +### `playerContent` + +对已识别网盘分享链接返回: + +```python +{"parse": 0, "playUrl": "", "url": id} +``` + +对未识别链接返回: + +```python +{"parse": 0, "playUrl": "", "url": ""} +``` + +不返回 `route/id` 风格结构。 + +## URL 与请求设计 + +### 主域名 + +- `https://1.star2.cn` + +### 基础请求头 + +- `User-Agent`:沿用用户给定的浏览器 UA +- `Referer`:`https://1.star2.cn` + +### 请求 helper + +实现两个内部 helper: + +- `_headers()`:生成带 cookie 的请求头 +- `_get_html(url)`:统一请求 HTML 文本 + +`_headers()` 规则: + +- 始终带 `User-Agent` +- 始终带 `Referer` +- 仅在缓存 cookie 非空时追加 `cookie` + +`_get_html(url)` 规则: + +- 统一通过 `self.fetch` 请求 +- 失败时返回空字符串或由上层解析成空结果 +- 不在 helper 内猜测页面编码之外的站点行为 + +## 页面解析策略 + +### HTML 解析方式 + +优先使用仓库现有 `self.html()` + XPath,而不是引入 `BeautifulSoup` 新依赖。 + +原因: + +- 与当前仓库其他蜘蛛一致 +- 单测更容易直接构造 HTML 片段 +- 避免为一个站点引入不同解析风格 + +### 列表与搜索卡片 + +站点参考选择器较深,但字段实际很少,只提取以下最稳定的数据: + +- `href` +- 文字标题 + +如节点缺失,直接跳过当前卡片,不做兜底猜测。 + +### 详情页分享链接 + +详情页只收集 `a[href]` 中真实可用的分享链接: + +- 空链接跳过 +- 重复链接去重 +- 未识别网盘类型的链接跳过 + +## 网盘线路规则 + +支持识别以下 9 类网盘: + +- `quark`:夸克 +- `ali`:阿里 +- `115`:115 +- `tianyi`:天翼 +- `uc`:UC +- `baidu`:百度 +- `xunlei`:迅雷 +- `123pan`:123 +- `yd`:移动云盘 + +域名识别规则: + +- `quark`:`quark` +- `115`:`115.com` +- `tianyi`:`cloud.189.cn` +- `uc`:`drive.uc.cn` 或 `uc.cn` +- `baidu`:`pan.baidu.com` +- `xunlei`:`xunlei` +- `123pan`:`123pan` +- `yd`:`caiyun` 或 `139.com` +- `ali`:`aliyundrive` 或 `alipan` + +### 排序与命名 + +线路输出顺序固定为: + +1. `quark` +2. `ali` +3. `115` +4. `tianyi` +5. `uc` +6. `baidu` +7. `xunlei` +8. `123pan` +9. `yd` + +每个线路名使用稳定 key,而不是展示中文: + +- `vod_play_from = "quark$$$ali$$$baidu"...` + +每个线路内的剧集项使用展示名加 `$` 拼接: + +- `夸克资源$https://pan.quark.cn/s/demo` +- `百度资源$https://pan.baidu.com/s/demo` + +多个同类链接使用 `#` 拼接。 + +### 组装规则 + +详情页提取到的分享链接经过以下流程: + +1. 去空 +2. 去重 +3. 识别网盘类型 +4. 按预定义顺序分组 +5. 生成 `vod_play_from` +6. 生成 `vod_play_url` + +设计上不为同类链接额外制造 `夸克1/夸克2` 这样的条目名。 + +原因: + +- 当前仓库同类盘站一般使用统一展示名 +- 用户最终消费的是分享链接,不依赖人工编号 +- 可减少无意义字符串差异,便于测试断言 + +## 异常与空结果策略 + +### 分类页 / 搜索页 + +当请求失败、HTML 为空或解析不到卡片时,返回空列表,但结构保持稳定。 + +### 详情页 + +当请求失败或解析失败时,返回: + +- `{"list": []}` + +不返回半残缺占位对象。 + +原因: + +- 当前仓库多数新蜘蛛对详情失败采取空列表回退 +- 比返回字段为空但看似成功的对象更容易让调用方识别失败 + +### `playerContent` + +不尝试修正、跳转或二次解析链接: + +- 支持的网盘链接直接透传 +- 非支持链接直接返回空 `url` + +## 测试设计 + +测试文件: + +- `py/tests/test_双星.py` + +使用 `unittest` 与 `unittest.mock`,全部通过 mock 隔离网络访问。 + +核心测试点: + +1. `homeContent` 返回固定 7 个分类 +2. `homeVideoContent` 返回空列表 +3. `init` 能从首页响应 cookies 组装出 `a=1; b=2` 形式的缓存 cookie +4. `_headers()` 在 cookie 存在和不存在时都返回正确请求头 +5. `categoryContent` 组装正确 URL 并解析列表卡片 +6. `searchContent` 组装正确搜索 URL 并解析结果 +7. `searchContent` 对空关键词直接返回空列表 +8. `detailContent` 能提取标题和多类网盘链接 +9. `detailContent` 会去重、过滤未知链接并按固定顺序组装线路 +10. `detailContent` 在详情页请求失败时返回空列表 +11. `playerContent` 透传支持的网盘链接 +12. `playerContent` 拒绝非网盘链接 + +## 实现约束 + +- 不引入新的第三方依赖 +- 不修改 `base/` 公共行为 +- 不在 `playerContent` 中引入路由分发语义 +- 不伪造 `pagecount` +- 不把完整 URL 存成列表页 `vod_id` + +## 验收标准 + +完成后应满足: + +- `homeContent(False)` 返回预期分类 +- `categoryContent/searchContent` 能按参考 URL 组织请求并解析基础卡片 +- `detailContent` 正确输出 `vod_play_from` 和 `vod_play_url` +- `playerContent` 对支持网盘链接返回可直接消费的透传结构 +- 新增单测全部通过