9.1 KiB
茶杯狐 Python 爬虫设计
目标
在当前 Python 仓库中新增一个符合 base.spider.Spider 接口的茶杯狐站点爬虫,完整覆盖以下能力:
- 首页分类
- 首页推荐
- 分类列表
- 搜索
- 详情解析
- 播放解析
- 搜索/详情/播放页面的人机验证绕过
实现基于站点 HTML 页面和 foxplay 接口,不引入服务端路由层,不修改 base/ 公共层,并保持当前仓库的短 ID 和结果结构约定。
范围
本次实现包含:
- 新增独立脚本,文件名为
茶杯狐.py - 新增对应单元测试
tests/test_茶杯狐.py - 使用单一站点主域:
https://www.cupfox.ai - 支持
home/homeVideo/category/search/detail/player主链路 - 实现命中验证页后的 Cookie 合并、
token提取和/robot.php过盾请求 - 实现
foxplay/api.php的两种播放链接解密 - 过滤 404 占位、403 和明显不可用链接,并在失败时回退
parse=1
本次实现不包含:
- 多域名探活与自动切换
- 浏览器自动化、JS 引擎执行和验证码识别
- 站外搜索源聚合和非站内播放链路修复
- 登录态管理和本地缓存持久化
方案选择
采用仓库现有的“单站点单文件 + 单测”方案:
- 对外保持
Spider接口兼容 - 对内拆分为短 ID 处理、请求与过盾、卡片解析、详情解析、播放解密几个 helper
- 只在站点文件内部维护逻辑,不修改
base/
不直接照搬参考 JS 路由层的原因是:
- 当前仓库只消费 Spider 接口,不消费 Node 路由插件
- Python 版已有稳定的 HTML 爬虫实现风格,应延续既有结构
- 本次重点是把茶杯狐能力转成可测试的 Python Spider,而不是保留一层额外运行时
模块边界
新增脚本内部职责拆分如下:
init- 初始化主域、请求头、分页大小和字符映射表
homeContent- 返回分类列表和静态筛选空结构
homeVideoContent- 请求首页并解析推荐卡片,去重后截断
categoryContent- 请求分类页,复用卡片解析并返回页码、总量估算和列表
searchContent- 请求搜索页,复用卡片解析并支持
quick
- 请求搜索页,复用卡片解析并支持
detailContent- 请求详情页,提取元数据和多线路播放列表
playerContent- 请求播放页、提取
player_aaaa、调用foxplay/api.php、解密直链并决定是否回退解析
- 请求播放页、提取
- 私有辅助函数
- URL 补全与短 ID 编解码
- HTML 请求与过盾流程
- Cookie 合并
token提取和过盾加密player_aaaa解析- 两套播放解密
- 占位/禁播链接识别
ID 与 URL 设计
遵循当前仓库“短 ID,不暴露完整 URL”的约定。
详情页短 ID:
- 页面链接
/movie/<slug>.html - 存储为
detail/<slug>
播放页短 ID:
- 页面链接
/play/<slug>.html - 存储为
play/<slug>
编码与解码原则:
- 只接受站内标准详情/播放路径
- 解析失败时返回空字符串,调用侧直接回空结果或回退
- 内部统一通过
urljoin补全相对路径
Host 与请求策略
本次只实现单域:
https://www.cupfox.ai
统一请求头至少包含:
User-AgentRefererAcceptAccept-Language
请求原则:
- HTML 请求超时固定为 15 秒
- 普通页面请求走
self.fetch/self.post - 页面请求默认不自动依赖持久会话,而是在单次过盾流程里显式维护 Cookie 字典
- 命中验证时只处理当前请求,不做全局 Cookie 缓存
- 请求失败时返回空结果或播放页回退,不抛出未处理异常
过盾设计
页面请求统一走 _request_with_firewall():
- 首次请求目标 URL,收集响应文本和
set-cookie - 如果页面不包含
人机验证或verifyBox,直接返回 HTML - 如果命中验证,从 HTML 中提取
var token = encrypt("...") - 对当前 URL 和原始 token 分别执行参考实现中的字符位移 + 随机填充 + Base64 加密
- 向
/robot.php发起表单 POST,请求头带上已收集 Cookie - 合并验证接口返回的 Cookie
- 带最新 Cookie 二次请求原页面
- 若二次请求仍是验证页,则按失败处理
该流程只用于 HTML 页面:
- 首页
- 分类页
- 搜索页
- 详情页
- 播放页
foxplay/api.php 不走过盾流程。
分类、首页与搜索设计
分类
分类从首页导航 nav.bm-item-list a 动态解析,提取:
type_idtype_name
只保留 /type/<id>.html 结构的导航链接。
首页推荐
首页推荐从 .mobile-main .panel .movie-list-item 提取,按详情 URL 去重后返回前 20 条。
分类列表
分类页路径:
/type/<type>-<page>.html
每张卡片提取:
- 链接:
a[href] - 标题:
a[title] - 封面:
.Lazy[data-original] - 备注:
.movie-item-note,为空时退回.movie-item-score
搜索
搜索页路径:
/search/<keyword>----------<page>---.html
搜索结果按 .vod-search-list .box 解析,字段提取规则与分类卡片一致,但标题优先取 .movie-title。
返回策略:
limit固定为 20- 列表和搜索返回
page、total、limit、list - 不返回
pagecount - 当页结果为空时
total为 0 - 当页结果非空时,
total至少保证大于等于page * len(list),避免上层立即判定无后续分页
详情页设计
详情页解析以下字段:
vod_idvod_namevod_picvod_contentvod_yearvod_directorvod_actorvod_play_fromvod_play_url
解析策略:
- 标题取
h1.movie-title - 海报取
.poster img - 简介取
.summary.detailsTxt的纯文本,并移除展开按钮文本 - 年份从
.scroll-content a中提取四位数字 - 导演和演员从
.info-data中按标签文案识别并拼接
播放列表策略:
- 线路名从
.play_source_tab .swiper-slide提取 - 每个
.play_list_box对应一组线路 - 集数条目从
.content_playlist li a提取为名称$play/<slug> - 最终按仓库约定拼接成
vod_play_from和vod_play_url
如果线路数和列表数不一致:
- 优先保留已有剧集列表
- 缺失的线路名回退为
线路<n>
播放解析设计
播放页提取
playerContent 先还原播放页 URL,请求 HTML,再从脚本中提取:
player_aaaa.urlplayer_aaaa.fromplayer_aaaa.server
如果找不到 player_aaaa.url,直接回退:
parse = 1url = 播放页 URLheader带User-Agent和当前页Referer
foxplay/api.php
当 player_aaaa.url 存在时:
- 以
vid=<player_aaaa.url>POST 到/foxplay/api.php - 若接口返回
data.url,按urlmode解密:1走Decode1.sign2走decode2- 其他值直接使用原始
url
- 对解密结果做占位链接识别
解密规则
Decode1.sign 对应参考实现的三段式处理:
- 自定义异或解码
- Base64 还原映射表
- 按明文/密文字母映射恢复真实路径
decode2 对应参考实现的字典反向位移逻辑:
- Base64 解码
- 每三个字符取中间位
- 按字典回退 3 位
占位链接识别
以下情况视为不可用直链:
- 空字符串
- 非
http(s)////magnet:链接 - 指向
404.mp4 - 参数含
code=403 - 包含
forbidden
播放返回策略
如果解密得到可用直链:
- 返回
parse = 0 url为真实地址header.Referer指向muiplayer.php?vid=...
如果 API 无直链、解密失败或命中占位链接:
- 返回
parse = 1 url为原播放页header.Referer指向原播放页
错误处理
所有对外方法在站点异常时返回兼容结构,不抛出未处理异常:
homeContent返回空分类homeVideoContent返回空列表categoryContent返回当前页和空列表searchContent返回当前页和空列表detailContent返回空listplayerContent返回原播放页的parse=1
内部异常处理原则:
- 私有 helper 可抛出
ValueError - 对外接口统一兜底
- JSON 解析、Base64 解码和映射解密失败时返回空字符串,不中断整个请求
测试设计
测试采用 unittest 和 unittest.mock,避免真实网络访问。
首批覆盖:
- 短 ID 编解码
- Cookie 合并和请求头拼装
- 验证页
token提取和过盾二次请求 - 首页分类解析
- 首页推荐去重
- 分类列表解析
- 搜索结果解析和空关键词分支
- 详情页元数据和多线路播放列表拼装
Decode1与decode2的解密行为playerContent的直链成功分支playerContent的占位链接回退分支playerContent的缺失player_aaaa回退分支
测试粒度要求:
- 先验证最小私有 helper
- 再验证对外接口方法
- 只 mock 网络层,不 mock 纯解析函数
验收标准
满足以下条件视为完成:
- 新增
茶杯狐.py,接口与仓库现有 Spider 一致 - 新增
tests/test_茶杯狐.py - 列表和搜索结果使用短详情 ID
- 详情页播放列表使用短播放 ID
- 命中验证页时能完成两次请求和 Cookie 合并
playerContent能在可用时返回直链,在不可用时稳定回退- 相关测试通过,且不引入
pagecount到列表/搜索返回结构