This commit is contained in:
tamakyi
2026-09-25 13:16:54 +08:00
commit ed4e47e305
32 changed files with 9214 additions and 0 deletions
+378
View File
@@ -0,0 +1,378 @@
// 爬虫相关
package main
import (
"bytes"
"fmt"
"sync"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/valyala/fasthttp"
"github.com/valyala/fastjson"
)
type httpClient struct {
client *fasthttp.Client
url string
body []byte
method string
cookies []*fasthttp.Cookie
userAgent string
contentType string
referer string
}
var defaultClient = &fasthttp.Client{
MaxIdleConnDuration: 90 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
var fetchRoomPool fastjson.ParserPool
// 直播间的数据结构
type liveRoom struct {
name string // 主播名字
title string // 直播间标题
liveID string // 直播 ID
}
// liveRoom 的 map
var liveRooms struct {
sync.RWMutex // rooms 的锁
rooms map[int]*liveRoom // 现在的 liveRoom
newRooms map[int]*liveRoom // 新的 liveRoom
}
// liveRoom 的 pool
var liveRoomPool = &sync.Pool{
New: func() any {
return new(liveRoom)
},
}
// 获取主播的直播链接
func getURL(uid int) string {
const livePage = "https://live.acfun.cn/live/%d"
return fmt.Sprintf(livePage, uid)
}
// 获取主播的直播链接
func (s *streamer) getURL() string {
return getURL(s.UID)
}
// http 请求,调用后需要 defer fasthttp.ReleaseResponse(resp)
func (c *httpClient) doRequest() (resp *fasthttp.Response, e error) {
defer func() {
if err := recover(); err != nil {
lPrintErrf("Recovering from panic in doRequest(), the error is: %v", err)
e = fmt.Errorf("请求 %s 时出错,错误为 %v", c.url, err)
fasthttp.ReleaseResponse(resp)
}
}()
if c.client == nil {
c.client = defaultClient
}
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
resp = fasthttp.AcquireResponse()
if c.url != "" {
req.SetRequestURI(c.url)
} else {
fasthttp.ReleaseResponse(resp)
return nil, fmt.Errorf("请求的 url 不能为空")
}
if len(c.body) != 0 {
req.SetBody(c.body)
}
if c.method != "" {
req.Header.SetMethod(c.method)
} else {
// 默认为 GET
req.Header.SetMethod(fasthttp.MethodGet)
}
if len(c.cookies) != 0 {
for _, cookie := range c.cookies {
req.Header.SetCookieBytesKV(cookie.Key(), cookie.Value())
}
}
const userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
if c.userAgent != "" {
req.Header.SetUserAgent(c.userAgent)
} else {
req.Header.SetUserAgent(userAgent)
}
if c.contentType != "" {
req.Header.SetContentType(c.contentType)
}
if c.referer != "" {
req.Header.SetReferer(c.referer)
}
req.Header.SetCookie("_did", deviceID)
req.Header.Set("Accept-Encoding", "gzip")
err := c.client.Do(req, resp)
checkErr(err)
return resp, nil
}
// 获取响应 body
func getBody(resp *fasthttp.Response) []byte {
if string(resp.Header.Peek("content-encoding")) == "gzip" || string(resp.Header.Peek("Content-Encoding")) == "gzip" {
body, err := resp.BodyGunzip()
if err == nil {
return body
}
}
return resp.Body()
}
// 获取全部 AcFun 直播间
func fetchAllRooms() bool {
for count := 10000; count < 1e9; count *= 10 {
rooms, all, err := fetchLiveRoom(count)
if err != nil {
lPrintErr(err)
return false
}
if all {
liveRooms.newRooms = rooms
return true
}
if count == 1e8 {
lPrintErr("获取正在直播的直播间列表失败")
}
}
return false
}
// 获取指定数量的 AcFun 直播间列表
func fetchLiveRoom(count int) (rooms map[int]*liveRoom, all bool, e error) {
defer func() {
if err := recover(); err != nil {
e = fmt.Errorf("fetchLiveRoom() error: %v", err)
}
}()
const liveListURL = "https://live.acfun.cn/api/channel/list?count=%d&pcursor=0"
client := &httpClient{
url: fmt.Sprintf(liveListURL, count),
method: fasthttp.MethodGet,
}
resp, err := client.doRequest()
checkErr(err)
defer fasthttp.ReleaseResponse(resp)
body := getBody(resp)
p := fetchRoomPool.Get()
defer fetchRoomPool.Put(p)
v, err := p.ParseBytes(body)
checkErr(err)
v = v.Get("channelListData")
if !v.Exists("result") || v.GetInt("result") != 0 {
panic(fmt.Errorf("无法获取AcFun直播间列表,响应为:%s", string(body)))
}
if string(v.GetStringBytes("pcursor")) != "no_more" {
return nil, false, nil
}
liveList := v.GetArray("liveList")
rooms = make(map[int]*liveRoom, len(liveList))
for _, live := range liveList {
uid := live.GetInt("authorId")
room := liveRoomPool.Get().(*liveRoom)
room.name = string(live.GetStringBytes("user", "name"))
room.title = string(live.GetStringBytes("title"))
room.liveID = string(live.GetStringBytes("liveId"))
rooms[uid] = room
}
return rooms, true, nil
}
// 根据 uid 获取主播的名字,可能需要检查返回是否为空
func getName(uid int) string {
liveRooms.RLock()
room, ok := liveRooms.rooms[uid]
if ok {
name := room.name
liveRooms.RUnlock()
return name
}
liveRooms.RUnlock()
_, room, err := tryFetchLiveInfo(uid)
if err != nil {
return ""
}
defer liveRoomPool.Put(room)
return room.name
}
// 根据 uid 获取主播直播间的标题
func getTitle(uid int) string {
liveRooms.RLock()
room, ok := liveRooms.rooms[uid]
if ok {
title := room.title
liveRooms.RUnlock()
return title
}
liveRooms.RUnlock()
if isLive, room, err := tryFetchLiveInfo(uid); err == nil {
defer liveRoomPool.Put(room)
if isLive {
return room.title
}
}
return ""
}
// 根据 uid 获取 liveID,结果准确,可能需要检查返回是否为空
func getLiveID(uid int) string {
if isLive, room, err := tryFetchLiveInfo(uid); err == nil {
defer liveRoomPool.Put(room)
if isLive {
return room.liveID
}
}
return ""
}
// 根据 uid 查看主播是否正在直播
func isLiveOn(uid int) bool {
liveRooms.RLock()
_, ok := liveRooms.rooms[uid]
liveRooms.RUnlock()
if ok {
return true
}
if isLive, room, err := tryFetchLiveInfo(uid); err == nil {
defer liveRoomPool.Put(room)
return isLive
}
return false
}
// 获取主播直播间的标题
func (s *streamer) getTitle() string {
return getTitle(s.UID)
}
// 获取 liveID,由于 AcFun 的 bug,结果不一定准确,可能需要检查返回是否为空
func (s *streamer) getLiveID() string {
liveRooms.RLock()
defer liveRooms.RUnlock()
room, ok := liveRooms.rooms[s.UID]
if ok {
return room.liveID
}
return ""
}
// 查看主播是否在直播,由于 AcFun 的 bug,结果不一定准确
func (s *streamer) isLiveOn() bool {
liveRooms.RLock()
defer liveRooms.RUnlock()
_, ok := liveRooms.rooms[s.UID]
return ok
}
// 获取用户直播相关信息,可能要将 room 放回 liveRoomPool
func fetchLiveInfo(uid int) (isLive bool, room *liveRoom, e error) {
defer func() {
if err := recover(); err != nil {
e = fmt.Errorf("fetchLiveInfo() error: %v", err)
}
}()
const acLiveInfo = "https://live.acfun.cn/api/live/info?authorId=%d"
client := &httpClient{
url: fmt.Sprintf(acLiveInfo, uid),
method: fasthttp.MethodGet,
}
resp, err := client.doRequest()
checkErr(err)
defer fasthttp.ReleaseResponse(resp)
body := getBody(resp)
p := fetchRoomPool.Get()
defer fetchRoomPool.Put(p)
v, err := p.ParseBytes(body)
checkErr(err)
if !v.Exists("result") || v.GetInt("result") != 0 {
return false, nil, fmt.Errorf("无法获取uid为%d的主播的直播信息,响应为:%s", uid, string(body))
}
room = liveRoomPool.Get().(*liveRoom)
if v.Exists("liveId") {
isLive = true
room.title = string(v.GetStringBytes("title"))
room.liveID = string(v.GetStringBytes("liveId"))
} else {
isLive = false
room.title = ""
room.liveID = ""
}
room.name = string(v.GetStringBytes("user", "name"))
return isLive, room, nil
}
// 获取用户直播相关信息,可能要将 room 放回 liveRoomPool
func tryFetchLiveInfo(uid int) (isLive bool, room *liveRoom, err error) {
err = runThrice(func() (err error) {
isLive, room, err = fetchLiveInfo(uid)
return err
})
return isLive, room, err
}
// 通过 wap 版网页查看主播是否在直播
func (s *streamer) isLiveOnByPage() (isLive bool) {
defer func() {
if err := recover(); err != nil {
lPrintErr("Recovering from panic in isLiveOnByPage(), the error is:", err)
lPrintErr("获取" + s.longID() + "的直播页面时出错")
}
}()
const acLivePage = "https://m.acfun.cn/live/detail/%d"
const mobileUserAgent = "Mozilla/5.0 (iPad; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
client := &httpClient{
url: fmt.Sprintf(acLivePage, s.UID),
method: fasthttp.MethodGet,
userAgent: mobileUserAgent,
}
resp, err := client.doRequest()
checkErr(err)
defer fasthttp.ReleaseResponse(resp)
body := getBody(resp)
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(body))
checkErr(err)
return doc.Find("p.closed-tip").Text() != "直播已结束"
}