Files
GoFilm/server/plugin/spider/Spider.go
2023-07-13 23:49:08 +08:00

338 lines
9.0 KiB
Go

package spider
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/url"
"server/config"
"server/model"
"server/plugin/common/dp"
"time"
)
/*
舍弃第一版的数据处理思路, v2版本
直接分页获取采集站点的影片详情信息
*/
/*
1. 选择一个采集主站点, mysql检索表中只存储主站点检索的信息
2. 采集多个站点数据
2.1 主站点的采集数据完整地保存相关信息, basicInfo movieDetail search 等信息
2.2 其余站点数据只存储 name(影片名称), playUrl(播放url), 存储形式 Key<hash(name)>:value([]MovieUrlInfo)
3. api数据格式不变, 获取影片详情时通过subTitle 去redis匹配其他站点的对应播放源并整合到主站详情信息的playUrl中
4. 影片搜索时不再使用name进行匹配, 改为使用 subTitle 进行匹配
*/
const (
MainSite = "https://cj.lzcaiji.com/api.php/provide/vod/"
)
type Site struct {
Name string
Uri string
}
// SiteList 播放源采集站
var SiteList = []Site{
// 备用采集站
//{"lz_bk", "https://cj.lzcaiji.com/api.php/provide/vod/"},
//{"fs", "https://www.feisuzyapi.com/api.php/provide/vod/"},
//{"su", "https://subocaiji.com/api.php/provide/vod/at/json"},
//{"bf", "https://bfzyapi.com/api.php/provide/vod/"},
//{"ff", "https://svip.ffzyapi8.com/api.php/provide/vod/"},
//{"lz", "https://cj.lziapi.com/api.php/provide/vod/"},
{"kk", "https://kuaikan-api.com/api.php/provide/vod/from/kuaikan"},
{"bf", "http://by.bfzyapi.com/api.php/provide/vod/"},
{"ff", "https://cj.ffzyapi.com/api.php/provide/vod/"},
}
// StartSpider 执行多源spider
func StartSpider() {
// 保存分类树
CategoryList()
log.Println("CategoryList 影片分类信息保存完毕")
// 爬取主站点数据
MainSiteSpider()
log.Println("MainSiteSpider 主站点影片信息保存完毕")
// 查找并创建search数据库, 保存search信息, 添加索引
time.Sleep(time.Second * 10)
model.CreateSearchTable()
SearchInfoToMdb()
model.AddSearchIndex()
log.Println("SearchInfoToMdb 影片检索信息保存完毕")
//获取其他站点数据13
go MtSiteSpider()
log.Println("Spider End , 数据保存执行完成")
time.Sleep(time.Second * 10)
}
// CategoryList 获取分类数据
func CategoryList() {
// 设置请求参数信息
r := RequestInfo{Uri: MainSite, Params: url.Values{}}
r.Params.Set(`ac`, "list")
r.Params.Set(`pg`, "1")
r.Params.Set(`t`, "1")
// 执行请求, 获取一次list数据
ApiGet(&r)
// 解析resp数据
movieListInfo := model.MovieListInfo{}
if len(r.Resp) <= 0 {
log.Println("MovieListInfo数据获取异常 : Resp Is Empty")
}
_ = json.Unmarshal(r.Resp, &movieListInfo)
// 获取分类列表信息
classList := movieListInfo.Class
// 组装分类数据信息树形结构
categoryTree := dp.CategoryTree(classList)
// 序列化tree
data, _ := json.Marshal(categoryTree)
// 保存 tree 到redis
err := model.SaveCategoryTree(string(data))
if err != nil {
log.Println("SaveCategoryTree Error: ", err)
}
}
// MainSiteSpider 主站点数据处理
func MainSiteSpider() {
// 获取分页页数
pageCount, err := GetPageCount(RequestInfo{Uri: MainSite, Params: url.Values{}})
// 主站点分页出错直接终止程序
if err != nil {
panic(err)
}
// 开启协程加快分页请求速度
ch := make(chan int, pageCount)
waitCh := make(chan int)
for i := 1; i <= pageCount; i++ {
ch <- i
}
close(ch)
for i := 0; i < config.MAXGoroutine; i++ {
go func() {
defer func() { waitCh <- 0 }()
for {
pg, ok := <-ch
if !ok {
break
}
list, e := GetMovieDetail(pg, RequestInfo{Uri: MainSite, Params: url.Values{}})
if e != nil {
log.Println("GetMovieDetail Error: ", err)
continue
}
// 保存影片详情信息到redis
if err = model.SaveDetails(list); err != nil {
log.Println("SaveDetails Error: ", err)
}
}
}()
}
for i := 0; i < config.MAXGoroutine; i++ {
<-waitCh
}
}
// MtSiteSpider 附属数据源处理
func MtSiteSpider() {
for _, s := range SiteList {
// 执行每个站点的播放url缓存
PlayDetailSpider(s)
log.Println(s.Name, "playUrl 爬取完毕!!!")
}
}
// PlayDetailSpider SpiderSimpleInfo 获取单个站点的播放源
func PlayDetailSpider(s Site) {
// 获取分页页数
pageCount, err := GetPageCount(RequestInfo{Uri: s.Uri, Params: url.Values{}})
// 出错直接终止当前站点数据获取
if err != nil {
log.Println(err)
return
}
// 开启协程加快分页请求速度
ch := make(chan int, pageCount)
waitCh := make(chan int)
for i := 1; i <= pageCount; i++ {
ch <- i
}
close(ch)
for i := 0; i < config.MAXGoroutine; i++ {
go func() {
defer func() { waitCh <- 0 }()
for {
pg, ok := <-ch
if !ok {
break
}
list, e := GetMovieDetail(pg, RequestInfo{Uri: s.Uri, Params: url.Values{}})
if e != nil || len(list) <= 0 {
log.Println("GetMovieDetail Error: ", err)
continue
}
// 保存影片播放信息到redis
if err = model.SaveSitePlayList(s.Name, list); err != nil {
log.Println("SaveDetails Error: ", err)
}
}
}()
}
for i := 0; i < config.MAXGoroutine; i++ {
<-waitCh
}
}
// SearchInfoToMdb 扫描redis中的检索信息, 并批量存入mysql
func SearchInfoToMdb() {
// 1. 从redis的Zset集合中scan扫描数据, 每次100条
var cursor uint64 = 0
var count int64 = 100
for {
infoList, nextStar := model.ScanSearchInfo(cursor, count)
// 2. 将扫描到的数据插入mysql中
model.BatchSave(infoList)
// 3.设置下次开始的游标
cursor = nextStar
// 4. 判断迭代是否已经结束 cursor为0则表示已经迭代完毕
if cursor == 0 {
return
}
}
}
// UpdateMovieDetail 定时更新主站点和其余播放源信息
func UpdateMovieDetail() {
// 更新主站系列信息
UpdateMainDetail()
// 更新播放源数据信息
UpdatePlayDetail()
}
// UpdateMainDetail 更新主站点的最新影片
func UpdateMainDetail() {
// 获取分页页数
r := RequestInfo{Uri: MainSite, Params: url.Values{}}
r.Params.Set("h", config.UpdateInterval)
pageCount, err := GetPageCount(r)
if err != nil {
log.Printf("Update MianStieDetail failed")
}
// 保存本次更新的所有详情信息
var ds []model.MovieDetail
// 获取分页数据
for i := 1; i <= pageCount; i++ {
list, err := GetMovieDetail(i, r)
if err != nil {
continue
}
// 保存更新的影片信息, 同类型直接覆盖
if err = model.SaveDetails(list); err != nil {
log.Printf("Update MianStieDetail failed, SaveDetails Error ")
}
ds = append(ds, list...)
}
// 整合详情信息切片
var sl []model.SearchInfo
for _, d := range ds {
// 通过id 获取对应的详情信息
sl = append(sl, model.ConvertSearchInfo(d))
}
// 调用批量保存或更新方法, 如果对应mid数据存在则更新, 否则执行插入
model.BatchSaveOrUpdate(sl)
}
// UpdatePlayDetail 更新最x小时的影片播放源数据
func UpdatePlayDetail() {
for _, s := range SiteList {
// 获取单个站点的分页数
r := RequestInfo{Uri: s.Uri, Params: url.Values{}}
r.Params.Set("h", config.UpdateInterval)
pageCount, err := GetPageCount(r)
if err != nil {
log.Printf("Update %s playDetail failed", s.Name)
}
for i := 1; i <= pageCount; i++ {
// 获取详情信息, 保存到对应hashKey中
list, e := GetMovieDetail(i, r)
if e != nil || len(list) <= 0 {
log.Println("GetMovieDetail Error: ", err)
continue
}
// 保存影片播放信息到redis
if err = model.SaveSitePlayList(s.Name, list); err != nil {
log.Println("SaveDetails Error: ", err)
}
}
}
}
// StartSpiderRe 清空存储数据,从零开始获取
func StartSpiderRe() {
// 删除已有的存储数据, redis 和 mysql中的存储数据全部清空
model.RemoveAll()
// 执行完整数据获取
StartSpider()
}
// =========================公共方法==============================
// GetPageCount 获取总页数
func GetPageCount(r RequestInfo) (count int, err error) {
// 发送请求获取pageCount
r.Params.Set("ac", "detail")
r.Params.Set("pg", "2")
ApiGet(&r)
// 判断请求结果是否为空, 如果为空直接输出错误并终止
if len(r.Resp) <= 0 {
err = errors.New("response is empty")
return
}
// 获取pageCount
res := model.DetailListInfo{}
err = json.Unmarshal(r.Resp, &res)
if err != nil {
return
}
count = int(res.PageCount)
return
}
// GetMovieDetail 处理详情接口请求返回的数据
func GetMovieDetail(pageNumber int, r RequestInfo) (list []model.MovieDetail, err error) {
// 防止json解析异常引发panic
defer func() {
if e := recover(); e != nil {
log.Println("GetMovieDetail Failed : ", e)
}
}()
// 设置分页请求参数
r.Params.Set(`ac`, `detail`)
r.Params.Set(`pg`, fmt.Sprint(pageNumber))
ApiGet(&r)
// 影视详情信息
details := model.DetailListInfo{}
// 如果返回数据为空则直接结束本次循环
if len(r.Resp) <= 0 {
err = errors.New("response is empty")
return
}
// 序列化详情数据
if err = json.Unmarshal(r.Resp, &details); err != nil {
return
}
// 处理details信息
list = dp.ProcessMovieDetailList(details.List)
return
}