package spider import ( "encoding/json" "errors" "fmt" "log" "net/url" "server/config" "server/model" "server/plugin/common" "time" ) /* 舍弃第一版的数据处理思路, v2版本 直接分页获取采集站点的影片详情信息 */ /* 1. 选择一个采集主站点, mysql检索表中只存储主站点检索的信息 2. 采集多个站点数据 2.1 主站点的采集数据完整地保存相关信息, basicInfo movieDetail search 等信息 2.2 其余站点数据只存储 name(影片名称), playUrl(播放url), 存储形式 Key:value([]MovieUrlInfo) 3. api数据格式不变, 获取影片详情时通过subTitle 去redis匹配其他站点的对应播放源并整合到主站详情信息的playUrl中 4. 影片搜索时不再使用name进行匹配, 改为使用 subTitle 进行匹配 */ const ( MainSite = "https://www.feisuzyapi.com/api.php/provide/vod/" ) type Site struct { Name string Uri string } // SiteList 播放源采集站 var SiteList = []Site{ //{"tk", "https://api.tiankongapi.com/api.php/provide/vod"}, //{"yh", "https://m3u8.apiyhzy.com/api.php/provide/vod/"}, //{"zk", "https://api.1080zyku.com/inc/apijson.php"}, 数据格式不规范,不采用 //{"fs", "https://www.feisuzyapi.com/api.php/provide/vod/"}, {"lz", "https://cj.lziapi.com/api.php/provide/vod/"}, {"ff", "https://cj.ffzyapi.com/api.php/provide/vod/"}, {"su", "https://subocaiji.com/api.php/provide/vod/at/json"}, } // StartSpider 执行多源spider func StartSpider() { // 保存分类树 CategoryList() log.Println("CategoryList 影片分类信息保存完毕") // 爬取主站点数据 MainSiteSpider() log.Println("MainSiteSpider 主站点影片信息保存完毕") // 查找并创建search数据库 time.Sleep(time.Second * 10) model.CreateSearchTable() SearchInfoToMdb() log.Println("SearchInfoToMdb 影片检索信息保存完毕") // 获取其他站点数据 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 := common.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", "1") 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 = common.ProcessMovieDetailList(details.List) return }