docs: add zxzjhd spider design

This commit is contained in:
Harold
2026-04-20 10:44:05 +08:00
parent 5efe589685
commit be456a02d9
@@ -0,0 +1,375 @@
# 在线之家 Python 爬虫设计
## 目标
在当前 Python 仓库中新增一个符合 `base.spider.Spider` 接口的在线之家站点爬虫,覆盖以下能力:
- 首页分类与筛选
- 分类列表
- 搜索
- 详情解析
- 播放解析
实现基于站点 HTML 页面结构,不引入 JS 服务端路由层,不修改 `base/` 公共层,并且保留详情页中的网盘分享线路。
## 范围
本次实现包含:
- 新增独立脚本,文件名为 `在线之家.py`
- 使用单一站点主域:`https://www.zxzjhd.com`
- 支持 `home/category/detail/search/player` 主链路
- 首页返回固定分类与静态筛选配置
- 详情页同时输出普通播放线路和网盘分享线路
- `playerContent` 对普通线路执行 `zxzj` 解密,对网盘线路直接透传分享链接
本次实现不包含:
- 参考 JS 中的 Fastify 路由封装
- 网盘驱动对象注入、驱动缓存映射、网盘目录展开
- 浏览器自动化或 JS 执行环境
- 多域名探活与自动切换
- 登录态、Cookie 常驻维护、反爬绕过
## 方案选择
采用仓库现有的“单站点单文件 + 单测”方案:
- 对外保持 `Spider` 接口兼容
- 对内拆分为请求封装、筛选路径拼接、列表解析、详情解析、播放解析几个 helper
- 保留参考 JS 规则里的关键业务行为,但只实现 Python Spider 实际需要的部分
不引入 JS 版 drive 处理层的原因是:
- 当前 Python 仓库没有统一的 drive 注入协议
- 现有同类蜘蛛对网盘资源的惯例是保留线路名与分享链接,让上层插件处理
- 本次重点是 HTML 解析、数据整形和 `zxzj` 播放解密,不是中间层编排
## 模块边界
新增脚本只在站点文件内部维护逻辑,不修改 `base/`
脚本内部职责拆分如下:
- `init`
- 初始化主域、请求头、固定分类、筛选项与筛选默认值
- `homeContent`
- 返回固定 `class``filters`
- `homeVideoContent`
- 返回空列表,不额外抓首页推荐
- `categoryContent`
- 根据分类、筛选和分页拼出 URL,请求 HTML 并解析卡片
- `searchContent`
- 根据关键词请求搜索页并解析结果
- `detailContent`
- 请求详情页,提取影片元数据、普通播放线路和网盘线路
- `playerContent`
-`zxzj` 普通线路做播放解密,对网盘线路直接透传分享链接
- 私有辅助函数
- 补全相对地址
- 清理文本
- 修复 JSON 包裹 HTML
- 拼接筛选路径
- 解析卡片列表
- 提取详情字段
- 提取标签页与剧集列表
- 提取播放页中的网盘分享链接
- 识别网盘类型
- 执行 `zxzj` 解密
## Host 与请求策略
本次只实现单域:
- `https://www.zxzjhd.com`
请求统一通过 `self.fetch` 发起,固定请求头至少包含:
- `User-Agent`
- `Referer: https://www.zxzjhd.com/`
请求原则:
- HTML 请求超时固定为 10 秒
- 请求失败时返回空 HTML 或空结果,不抛出未处理异常
- 对字符串响应先尝试修复 JSON 包裹 HTML 的情况,避免站点返回 `"<!doctype html...>"` 这种包裹值时解析失败
- 不引入浏览器执行,也不依赖外部解压、解密服务
## 分类与筛选设计
首页分类固定为:
- `1 -> 电影`
- `2 -> 美剧`
- `3 -> 韩剧`
- `4 -> 日剧`
- `5 -> 泰剧`
- `6 -> 动漫`
筛选配置直接内置到脚本中,字段与参考实现保持一致:
- `class`
- `area`
- `year`
- `by`
筛选默认值保留 `cateId`,通过分类 id 决定。
筛选 URL 规则采用模板拼接:
- `{{fl.cateId}}-{{fl.area}}-{{fl.by}}-{{fl.class}}-----fypage---{{fl.year}}`
输出路径再包到:
- `/vodshow/<filter-path>.html`
规则说明:
- `cateId` 来自当前分类默认配置
- `class/area/year/by` 来自传入的 `extend`
- 缺失筛选值时保留空段,不重排位置
- 当前仓库约定不返回 `pagecount`,分类结果只返回 `list/page/limit/total`
`homeContent` 返回:
- `class`
- `filters`
不返回首页推荐列表。
## 列表与搜索设计
分类页和搜索页均解析 `stui-vodlist` 卡片结构。
卡片提取字段:
- 标题:`a[title]`
- 图片:`data-original`,缺失时回退到 `data-src``src`
- 备注:`.pic-text`
- 链接:`a[href]`
输出统一卡片结构:
- `vod_id`
- `vod_name`
- `vod_pic`
- `vod_remarks`
其中 `vod_id` 保留短详情路径,而不是完整 URL,形式为:
- `voddetail/12345.html`
或等价的相对详情路径片段。
这样可以遵循仓库当前短 id 约定,避免把完整域名泄露到列表结果里。
搜索接口使用:
- `/vodsearch/<关键词>-------------.html`
搜索直接请求完整搜索页 HTML,不做 POST,也不做二次过滤。
分页策略采用保守估计:
- 分类页 `limit` 固定为 24
- 搜索结果 `limit` 使用结果数
- `total` 采用 `page * 30 + len(items)` 的仓库常见保守写法
- 不返回 `pagecount`
## 详情页设计
详情页解析以下字段:
- 标题
- 封面
- 简介
- 年份
- 地区
- 类型
- 导演
- 主演
- 更新信息或集数备注
详情输出字段至少包含:
- `vod_id`
- `vod_name`
- `vod_pic`
- `vod_content`
- `vod_remarks`
- `vod_year`
- `vod_area`
- `vod_class`
- `vod_lang`
- `vod_director`
- `vod_actor`
- `vod_play_from`
- `vod_play_url`
图片地址统一补全为完整 URL,修复站点相对路径与 `//` 形式地址。
详情页播放来源分为两组:
1. 普通站内线路
- 来源于详情页的播放标签和对应剧集列表
- 不保留原站复杂线路名,统一归并成 `zxzj`
- 每条剧集格式为 `名称$<play-id>`
- 同一普通线路下多集使用 `#` 连接
2. 网盘线路
- 来源于详情页标签中明显标记为网盘、百度、夸克、UC、阿里、迅雷的线路
- 详情页中的剧集链接先指向站内播放页,不直接给出分享链接
- 需要继续请求这些播放页,读取其中 `player_aaaa.url`
- 如果 `player_aaaa.url` 是分享地址,则按网盘类型分组保留
网盘线路识别范围:
- `baidu`
- `quark`
- `uc`
- `aliyun`
- `xunlei`
网盘类型优先通过分享链接域名识别:
- `pan.baidu.com` -> `baidu`
- `pan.quark.cn` -> `quark`
- `drive.uc.cn` -> `uc`
- `alipan.com``aliyundrive.com` -> `aliyun`
- `pan.xunlei.com` -> `xunlei`
如果标签名称与域名不一致,以分享链接域名为准。
网盘线路保留策略:
- 每种网盘类型单独作为一条线路
- 每个条目格式为 `剧集名$分享链接`
- 按分享链接去重,保留第一次出现的标题
- 没有有效分享链接的网盘项直接跳过
最终线路组合规则:
- `vod_play_from``zxzj$$$quark$$$baidu` 这类拼接形式
- `vod_play_url` 与线路顺序严格对应
- 没有普通线路时,不强行保留 `zxzj`
- 没有网盘线路时,只返回普通线路
## 播放设计
`playerContent(flag, id, vipFlags)` 按来源分流。
### 普通线路 `zxzj`
输入 `id` 为相对播放路径或短播放 id 时,先补全为完整播放页 URL,再执行以下流程:
1. 请求播放页
2. 提取页面中指向 `jx.zxzj` 的中间页 URL
3. 请求中间页
4. 提取 `result_v2` JSON 中的加密串
5. 执行参考实现中的解密算法:
- 先翻转字符串
- 按两位十六进制转字符
- 移除中间插入的 7 位混淆字符
6. 如果解密结果为媒体直链,则返回直链
成功返回:
- `parse = 0`
- `jx = 0`
- `url = 解密后的媒体地址`
- `header.Referer = 中间页 URL`
如果任一步失败,则回退为原播放页地址,返回:
- `parse = 1`
- `jx = 1`
- `url = 播放页 URL`
### 网盘线路
`flag` 为以下值之一时视为网盘线路:
- `baidu`
- `quark`
- `uc`
- `aliyun`
- `xunlei`
对网盘线路不做二次解析,直接透传分享链接:
- `parse = 0`
- `jx = 0`
- `url = 原始分享链接`
- `header = {}`
这样可以保持和仓库内现有带网盘蜘蛛的一致行为,让上层网盘插件继续接管。
## 解析与兼容性策略
为了降低页面结构波动影响,实现上采用“XPath/HTML 结构优先,正则兜底”的策略:
- 列表与搜索主走 XPath
- 详情字段主走 XPath 或相邻文本提取
- 播放页 `player_aaaa` 与中间页 `result_v2` 使用正则提取
- 网盘分享链接通过播放页内嵌 JSON 提取
文本处理原则:
- 缺失字段返回空字符串
- 标题、备注、简介统一做空白折叠
- 多值字段使用逗号连接
- 简介保留段落换行,不保留多余空白
URL 处理原则:
- 相对路径补全为完整 URL
- 列表和搜索输出短 `vod_id`
- 详情内普通剧集输出短 `play-id`
- 网盘线路保留完整分享链接
## 测试设计
新增测试文件:
- `tests/test_在线之家.py`
测试全部使用 `unittest``unittest.mock`,不访问真实网络,覆盖以下行为:
1. 首页
- 断言分类 id 和筛选 key 正确输出
2. 分类 URL
- 断言筛选模板拼接出的路径符合站点规则
3. 列表解析
- 断言能提取短 `vod_id`、标题、封面和备注
4. 搜索解析
- 断言能解析搜索页卡片
5. 详情解析
- 断言能提取标题、封面、简介、年份、地区、类型、导演、主演
- 断言普通线路统一输出为 `zxzj`
- 断言网盘播放页会被继续请求并转成 `baidu/quark/uc/aliyun/xunlei` 线路
- 断言网盘链接按分享链接去重
6. 播放解密
- 断言 `zxzj` 解密算法能还原媒体地址
- 断言 `playerContent` 对普通线路成功返回直链
- 断言解密失败时回退到播放页地址
7. 网盘播放分流
- 断言 `playerContent``baidu/quark/uc/aliyun/xunlei` 直接透传分享链接
## 风险与约束
- 站点反爬较强,真实页面可能偶发返回包裹字符串或异常结构,因此实现只保证对已知 HTML 结构稳健解析
- 网盘分享链接需要额外请求网盘播放页,详情阶段请求数会比普通站点更高
- 普通线路统一命名为 `zxzj` 是刻意收敛行为,丢弃了站点原始标签名,但换来更稳定的播放分流
- 该实现不承担网盘目录展开和鉴权,依赖上层现有网盘插件消费分享链接
## 验收标准
完成后应满足以下结果:
- 新增 `在线之家.py`,且不修改 `base/`
- `home/category/search/detail/player` 五条主链路都有单测覆盖
- 列表和搜索输出短 `vod_id`
- 详情页能同时输出普通线路和网盘线路
- 网盘线路按 `baidu/quark/uc/aliyun/xunlei` 标准名返回
- `playerContent("zxzj", ...)` 能执行解密并优先返回直链
- `playerContent(<网盘线路>, ...)` 直接返回分享链接
- 相关单测可在本仓库的 `unittest` 环境中稳定运行