引擎模板¶
对于定向的资源爬取任务,往往有特定的模式可循。API 框架将视频、弹幕、小说、漫画、音乐 这些抓取任务的工作模式进行了抽象,将数据抓取、处理所需要的常用功能进行了封装, 减少编写引擎的难度。
框架会自动伪装每一个请求、对获取的数据加标记、过滤清洗,最后以统一的 JSON 接口展现出来。 数据的解析是逐步进行的,只有当你访问某个的接口时,它才会调度相关引擎去完成进一步的解析。 解析过程中某些数据会自动缓存,以加快处理速度。对于寿命短暂的直链,API 通过动态重定向为资源续命。 对于存在防盗链的资源,它也能作为代理服务器,访问原始数据流并返回响应给前端,完成视频的播放……
这里面有许多繁琐无聊的事情, 好在 API 框架为你做了这些脏活,你只需要按照特定的模式, 一步一步提取数据就好。但是,这仍然是一件具有挑战性的事情, 因为你无法预料目标网站或者APP使用了何种手段阻止你抓取数据,不知道它将某些关键参数藏到了哪里, 即便已经接近成功,它仍可能返回一些稀奇古怪的数据迷惑你。数据的抓取是最困难的事情, 你可能遇到网页端反调试、js加密、接口数据加密、APP加壳、资源数据混淆等等问题, 这里有不少的坑,我个人的一些经验总结在了 爬虫技巧 一栏, 如果遇到问题,可以看看,说不定能得到启发。
OK,说了一大堆,现在我们终于要开始了!
现在,你盯上了一个网站或者APP,觉得它的资源很不错,可是广告满天飞, 于是你想把它做成一个引擎添加进来…
如何添加引擎¶
写好的引擎不要扔,裹上鸡蛋液,粘上面包糠, 扔到下面对应的目录下:
api
├─anime # 视频搜索
├─danmaku # 弹幕搜索
├─comic # 漫画搜索
└─music # 音乐搜索
然后, 在 api/config.json 中增加一个对应的配置项, 如:
{
"anime": [
{
"name": "歪比巴卜",
"notes": "这里写点简介",
"module": "api.anime.wbbb",
"type": [
"动漫"
],
"enable": false,
"quality": 8
}
]
}
module 是该引擎的模块名,与文件路径保持一致
enable 表示是否启用该引擎
quality 表示资源的整体质量 0~10 分
视频搜索引擎¶
视频的搜索解析模板:
from api.core.anime import *
from api.core.proxy import StreamProxy
class MyEngine(AnimeSearcher):
async def search(self, keyword: str):
"""
实现本方法, 搜索剧集摘要信息, 返回异步生成器
"""
html = "keyword 对应的网页内容"
items = ["剧集1的摘要信息", "剧集2的摘要信息", "剧集3的摘要信息"]
for item in items:
meta = AnimeMeta()
# 对 item 进行解析, 提取以下数据
meta.title = "番剧名称"
meta.category = "分类"
meta.desc = "简介"
meta.cover_url = "封面图 URL"
meta.detail_url = "详情页链接或参数"
yield meta # 产生一个结果交给下一级处理
class MyDetailParser(AnimeDetailParser):
async def parse(self, detail_url: str):
"""
实现本方法, 解析摘要信息中的链接, 获取详情页数据
"""
detail = AnimeDetail()
# detail_url 就是上面搜索结果中提取的内容
# 从详情页提取以下信息
detail.title = "番剧名称"
detail.cover_url = "封面图 URL"
detail.desc = "简介"
detail.category = "分类"
playlists = ["播放列表1的信息", "播放列表2的信息"]
for playlist in playlists:
pl = AnimePlayList()
# 解析播放列表的 html, 提取播放列表名和列表内容
pl.name = "播放列表名"
for item in playlist:
anime = Anime()
# 解析列表中的一集视频, 提取视频名字和 URL 信息
anime.name = "某一集视频的名字"
anime.raw_url = "视频的原始链接或者参数"
pl.append(anime)
detail.append_playlist(pl)
return detail
class MyUrlParser(AnimeUrlParser):
async def parse(self, raw_url: str) -> Union[AnimeInfo, str]:
"""
实现本方法, 解析某一集视频的原始链接, 获取直链和有效期
如果在详情页已经提取到了有效的直链, 可以不写这个类, 但通常是需要的
"""
# raw_url 是从详情页提取的信息
real_url = "根据 raw_url 找到的视频直链"
return real_url # 直接返回直链是可以的, 框架会尝试自行推断该直链对应的视频信息
# 如果你知道这个直链的信息就最好不过了, 省得框架去推测, 因为这不一定准确
# lifetime 视频的剩余寿命(秒), 如果视频过期, 框架将重新解析一次直链
# fmt 是视频格式, 可选 mp4 flv hls, 这将给前端播放器一个提示, 以便选择正确的解码器播放
# volatile 表示视频直链是否在访问后立即失效, 如果为 True, 则每次前端请求视频数据时, 框架都会重新解析直链
# 这些参数不要求全部提供, 你知道多少填多少(当然越多越好), 剩下的交给框架去推测
return AnimeInfo(real_url, lifetime=600, fmt="mp4", volatile=True) # 返回 AnimeInfo 对象
class MyVideoProxy(StreamProxy):
"""
本类用于实现视频流量的代理, 下面的方法按需重写
框架默认的实现可以应付大多数情况, 如果碰到一些稀奇古怪的情况, 你可能通过重写下面的某些方法
通常 mp4 视频需要绕过防盗链, 重写 set_proxy_headers 方法即可
许多 APP 会将 hls(m3u8) 视频片段隐藏到图片中, 需要重写 fix_chunk_data 方法剔除图片数据
其它方法大都用于处理 m3u8 文本文件, 正常情况无需重写
"""
def set_proxy_headers(self, real_url: str) -> dict:
"""
如果服务器存在防盗链, 需要检测 Referer 和 User-Agent, 可以尝试重写本方法
本方法可为特定的直链设置代理 Headers
若本方法返回空则使用默认 Headers
若设置的 Headers 不包含 User-Agent 则随机生成一个
"""
if "foo.bar" in real_url:
return {"Referer": "http://www.foo.bar"}
async def get_m3u8_text(self, index_url: str) -> str:
"""
获取 index.m3u8 文件的内容, 如果该文件需要进一步处理,
比如需要跳转一次才能得到 m3u8 的内容,
或者接口返回的数据经过加密、压缩时, 请重写本方法以获取 m3u8 文件的真实内容
:param index_url: index.m3u8 文件的链接
:return: index.m3u8 的内容
"""
return await self.read_text(index_url)
def fix_m3u8_key_url(self, index_url: str, key_url: str) -> str:
"""
修复 m3u8 密钥的链接(通常使用 AES-128 加密数据流),
默认以 index.m3u8 同级路径补全 key 的链接,
其它情况请重写本方法
:param index_url: index.m3u8 的链接
:param key_url: 密钥的链接(可能不完整)
:return: 密钥的完整链接
"""
if key_url.startswith("http"):
return key_url
path = '/'.join(index_url.split('/')[:-1])
return path + '/' + key_url
def fix_m3u8_chunk_url(self, index_url: str, chunk_url: str) -> str:
"""
替换 m3u8 文件中数据块的链接, 通常需要补全域名,
默认情况使用 index.m3u8 的域名补全数据块域名部分,
其它情况请重新此方法
:param index_url: index.m3u8 的链接
:param chunk_url: m3u8 文件中数据块的链接(通常不完整)
:return: 修复完成的 m3u8 文件
"""
if chunk_url.startswith("http"): # url 无需补全
return chunk_url
elif chunk_url.startswith('/'):
return extract_domain(index_url) + chunk_url
else:
return extract_domain(index_url) + '/' + chunk_url
def fix_chunk_data(self, url: str, chunk: bytes) -> bytes:
"""
修复数 m3u8 数据据块, 用于解除数据混淆
比如常见的图片隐写, 每一段视频数据存放于一张图片中, 需要剔除图片的数据
可使用 binwalk 等工具对二进制数据进行分析, 以确定图像与视频流的边界位置
:param url: 数据块的链接
:param chunk: 数据块的二进制数据
:return: 修复完成的二进制数据
"""
return chunk
弹幕搜索引擎¶
弹幕引擎模板:
from api.core.danmaku import *
class MyEngine(DanmakuSearcher):
async def search(self, keyword: str):
"""
实现本方法, 搜索弹幕摘要信息, 返回异步生成器
"""
html = "keyword 对应的网页内容"
items = ["番剧1的弹幕信息", "番剧2的弹幕信息"]
for item in items:
meta = DanmakuMeta()
# 解析 item 提取下列信息
meta.title = "番剧名称"
meta.play_url = "播放页链接或参数"
meta.num = 10 # 包含的集数
yield meta # 产生一个结果就交给上一级处理
class MyDetailParser(DanmakuDetailParser):
async def parse(self, play_url: str):
"""
解析番剧对应的弹幕的播放列表
"""
detail = DanmakuDetail()
items = ["第1集的弹幕信息", "第2集的弹幕信息"]
for ep in items:
danmaku = Danmaku()
danmaku.name = "本集视频的名字"
danmaku.cid = "解析弹幕数据需要的参数或链接"
detail.append(danmaku)
return detail
class MyDanmakuDataParser(DanmakuDataParser):
async def parse(self, cid: str):
"""
解析弹幕数据
"""
result = DanmakuData()
data = ["一条弹幕", "一条弹幕"]
for item in data:
result.append_bullet(
time=31.4, # 距离视频开头的秒数(float)
pos=1, # 位置参数(0右边, 1上边, 2底部)
color=int("ffffff", 16), # 如果颜色是 16 进制, 先转 10 进制
message="弹幕内容"
)
# 也可以使用 append 方法添加弹幕
result.append([123, 1, 16777215, "弹幕内容"])
return result
漫画搜索引擎¶
还没开始整,再等等~~
小说搜索引擎¶
还没开始整,再等等~~
音乐搜索引擎¶
还没开始整,再等等~~