爬爬爬爬爬某企鵝的影片,加了代理和定時任務

賭賭賭賭賭賭賭聖!發表於2020-12-16

今天心血來潮,想用GO寫爬蟲,參考了網上的程式碼粗略寫了一個。。。
初學GO,望大家多多指教~

思路:
1.從某企鵝影片網站 xxxxx.com/search.html?act=102&keyWord=XXX 中搜尋某個明星的影片。
2.在影片的html程式碼中用 GO 的github.com/PuerkitoBio/goquery 包解析標籤拿到影片地址(如地址為 XX.COM?XXX&XXX&XX&VID=123, 目標:要拿到123),再根據GO的字串擷取拿到VID。
3.從某企鵝影片網站的JS檔案中找到影片詳情的介面(如 XXX.COM?callback=XXX&VID=123,JS打斷點找到的),將上一步拿到的VID帶入進去,拿到影片詳情(jsonp格式資料:包含影片圖片,播放地址,播放量,播放時長等等資訊)。
4.用GO的字串擷取把jsonp擷取為json資料,使用 json.Unmarshal 解析到提前準備好的STRUCT 上面。
5.定義切片,把拿到的一個個影片詳情append到切片中。
6.遍歷切片,資料寫入到資料庫(由於GORM V1不支援批次寫入(V2支援哈),得手動拼接SQL寫入資料庫)。
7.POSTMAN上看GO爬影片所花的時間。
開始:

package models

import (
    "bytes"
    "encoding/json"
    "fmt"
    "github.com/PuerkitoBio/goquery"
    "io/ioutil"
    "log"
    "math/rand"
    "net/http"
    "net/url"
    "strconv"
    "strings"
    "time"
    "video/gin/databases"
)

// 從這個到下面的struct都是企鵝影片詳情的結構
type VideoDetail struct {
    Vl      Vl  `json:"vl"`
    Preview int `json:"preview"`
}

type Vl struct {
    Vi []Vi `json:"vi"`
}

type Vi struct {
    Fn    string `json:"fn"`
    Fvkey string `json:"fvkey"`
    Ul    Ui     `json:"ul"`
    Ti    string `json:"ti"`
}

type Ui struct {
    Ui []UiData `json:"ui"`
}

type UiData struct {
    Url string `json:"url"`
}

func (spider *Spider) SpiderAndInsertVideo() ([]NewStar, error) {
    // 請求明星資料的介面,後面企鵝影片搜尋需要明星的名字
    // 傳送請求,拿到明星的json資料,對映到struct中
    starUrl := "https://XXXXXX/S/dex?page=1&pagesize=50"

    client := http.Client{}
    req, err := http.NewRequest("POST", starUrl, nil)

    if err != nil {
        log.Printf("http.NewRequest star err:%v", err)
        return nil, err
    }

    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    var spiderStar SpiderStar
    err = json.Unmarshal([]byte(body), &spiderStar)

    if err != nil {
        return nil, err
    }

    // 我會把自己庫的明星資料刪掉,更換從介面拿到的最新的明星資料
    // 拼接字串,批次寫入明星資訊
    var buffer bytes.Buffer

    sql := "insert into `stars` (`name`, `created_at`, `updated_at`) values "
    if _, err := buffer.WriteString(sql); err != nil {
        return nil, err
    }

    for i, item := range spiderStar.Data.List {
        if i == len(spiderStar.Data.List)-1 {
            buffer.WriteString(fmt.Sprintf("('%s','%s','%s');", item.Name, time.Now().Format("2006-01-02 15:04:05"), time.Now().Format("2006-01-02 15:04:05")))
        } else {
            buffer.WriteString(fmt.Sprintf("('%s','%s','%s'),", item.Name, time.Now().Format("2006-01-02 15:04:05"), time.Now().Format("2006-01-02 15:04:05")))
        }
    }

    err = databases.DB.Exec("Delete From stars").Debug().Error

    if err != nil {
        return nil, err
    }

    err = databases.DB.Exec(buffer.String()).Debug().Error

    if err != nil {
        return nil, err
    }

    // 後面需要隨機獲取影片的播放量,做的假資料,真實資料太真實了,吸引不到人
    playCount := [4]string{
        "3萬+", "5萬+", "7萬+", "10萬+",
    }

    // 根據明星的名字開始爬TX的介面
    // 這裡我又從資料庫取資料,當時為了把這個資料返回出來驗證資料庫寫入的明星資料是否正確,其實可以直接拿介面的
    var stars []NewStar
    err = databases.DB.Table("new_stars").Find(&stars).Error

    if err != nil {
        return nil, err
    }

    for _, item := range stars {
        // 加代理
        agentQueryString, err := spider.GetAgentQueryString()

        if err != nil {
            return nil, err
        }

        agentUrl := config.AGENT_URL
        proxy := func(_ *http.Request) (*url.URL, error) {
            return url.Parse("http://"+agentUrl)
        }

        agentClient := &http.Client{
            Transport:&http.Transport{
                Proxy:proxy,
                TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
                ProxyConnectHeader : map[string][]string{
                    "Proxy-Authorization" : {agentQueryString},
                },
            },
            Timeout:time.Duration(8*time.Second),
        }

        // 用代理請求站長工具的網頁,測試代理是否OK,後面會返回截圖看代理是否正常
        ipUrl := "https://ip.tool.chinaz.com/"
        req1, err := http.NewRequest("GET", ipUrl, nil)

        if err != nil {
            fmt.Printf("http.NewRequest chinaz err:%v", err)
            continue
        }

        req1.Header.Add("Proxy-Authorization", agentQueryString)

        resp1, err := agentClient.Do(req1)
        if err != nil {
            fmt.Printf("http.NewRequest chinaz do err:%v", err)
            continue
        }

        // 拿到站長頁面的元素,只是測試用
        body, err := ioutil.ReadAll(resp1.Body)
        if err != nil {
            return nil, err
        }
        fmt.Println(string(body))


        // 用代理爬影片搜尋頁面,拿到明星的影片html標籤集合
        var allData []map[string]interface{}
        spiderListUrl := "https://m.v.XX.com/search.html?act=102&keyWord=" + url.QueryEscape(star.Name)
        req, err := http.NewRequest("GET", spiderListUrl, nil)

        if err != nil {
            log.Printf("http.NewRequest vid spider err:%v", err)
            continue
        }

        resp, err := agentClient.Do(req)
        if err != nil {
            log.Printf("http.NewRequest vid do err:%v", err)
            continue
        }

        document, err := goquery.NewDocumentFromReader(resp.Body)
        if err != nil {
            log.Printf("http.NewRequest vid document err:%v", err)
            continue
        }
        // 遍歷標籤,獲取影片ID
        document.Find(".search_item_h").EachWithBreak(func(i int, selection *goquery.Selection) bool {
            urlString, boolUrl := selection.Find("a").Attr("href")

            if !boolUrl {
                return false;
            }
            // 字串擷取,獲取vid
            // 如得到 http://xxx.com/page/k/3/q/k3137yyxmsq.html 要拿到 k3137yyxmsq
            start := strings.LastIndex(urlString, "/")
            end := strings.LastIndex(urlString, ".")
            vid := urlString[start+1 : end]

            // 調影片詳情介面獲取影片詳情
            videoDetailUrl := "https://xxxx.com/getxxxx?callback=xxxxxx&ge=0&t=auto&pe=json&id=ab40fd9fca45e4&id=e8af7e38009=v3010&r=0&ap=3.4.40&st=xxxx.com&t=httpsF%2Fxxx.com%2Fx%l%2Fdeo%2Frn&re=xxxx.com&ss=1&sps=d=" + strconv.Itoa(int(time.Now().Unix())) + "&spm=4&id=" + vid + "e=auo&ch=&show1080p=fals&e=1&clip=4&rc=&f=ao&derc=1&_=C103p%2BIKA10pd%3D&2=jw5b401F2g%3D01953="

            req, err := http.NewRequest("GET", videoDetailUrl, nil)

            if err != nil {
                log.Printf("http.NewRequest detail spider err:%v", err)
                return false // 這裡的return false 是跳出迴圈用的
            }

            resp, err := client.Do(req)
            if err != nil {
                log.Printf("http.NewRequest detail do spider err:%v", err)
                return false // 這裡的return false 是跳出迴圈用的
            }

            body, err := ioutil.ReadAll(resp.Body)
            if err != nil {
                log.Printf("http.NewRequest detail read spider err:%v", err)
                return false
            }

            // 得到的是jsonp資料,要轉json
            startPosition := strings.Index(string(body), "(")
            endPosition := strings.LastIndex(string(body), ")")
            jsonString := string(body)[startPosition+1 : endPosition]

            var videoDetail VideoDetail
            err = json.Unmarshal([]byte(jsonString), &videoDetail)

            if err != nil {
                log.Printf("http.NewRequest detail unmarshal spider err:%v", err)
                return false // 這裡的return false 是跳出迴圈用的
            }

            // 去掉不合法的影片
            title := videoDetail.Vl.Vi[0].Ti
            if strings.Contains(title, "抖音") ||
                strings.Contains(title, "douyin") ||
                strings.Contains(title, "抽菸") ||
                strings.Contains(title, "整容") ||
                strings.Contains(title, "吸菸") {
                return false // 這裡的return false 是跳出迴圈用的
            }

            // 去掉重複資料
            var has int
            err = databases.DB.Table("new_star_videos").Debug().Where("video_id = ?", vid).Count(&has).Error

            if err != nil || has > 0 {
                return false // 這裡的return false 是跳出迴圈用的
            }

            // 拿到影片詳情
            starId, _ := strconv.Atoi(star.Star)
            allData = append(allData, map[string]interface{}{
                "date":        time.Now().Format("2006-01-02"),
                "video_id":    vid,
                "url":         videoDetail.Vl.Vi[0].Ul.Ui[0].Url + videoDetail.Vl.Vi[0].Fn + "?vkey=" + videoDetail.Vl.Vi[0].Fvkey,
                "title":       title,
                "expire_time": time.Now().Unix() + 3600*4,
                "thumb":       "http://puui.qpic.cn/qqvideo_ori/0/" + vid + "_496_280/0",
                "star_id":     starId,
                "play_time":   strconv.Itoa(videoDetail.Preview),
                "play_count":  playCount[rand.Intn(3)],
                "source_type": 1,
                "source_name": "騰訊影片",
                "sort":        rand.Intn(999),
                "status":      1,
                "created_at":  time.Now().Format("2006-01-02 15:04:05"),
                "updated_at":  time.Now().Format("2006-01-02 15:04:05"),
            })
            return true
        })

        // 拼接拿到的影片資訊字串,批次寫入到影片表
        var buffer bytes.Buffer

        sql := "insert into `new_star_videos` (`date`,`star_id`,`video_id`, `expire_time`, `title`, `thumb`, `url`, `play_count`, `play_time`, `source_type`, `source_name`, `sort`, `status`, `created_at`, `updated_at`) values"
        if _, err := buffer.WriteString(sql); err != nil {
            continue
        }

        for i, item := range allData {
            if i == len(allData)-1 {
                buffer.WriteString(fmt.Sprintf("('%s', %d,'%s',%d,'%s','%s','%s','%s','%s',%d,'%s',%d,%d,'%s','%s');", item["date"], item["star_id"], item["video_id"], item["expire_time"], item["title"], item["thumb"], item["url"], item["play_count"], item["play_time"], item["source_type"], item["source_name"], item["sort"], item["status"], item["created_at"], item["updated_at"]))
            } else {
                buffer.WriteString(fmt.Sprintf("('%s', %d,'%s',%d,'%s','%s','%s','%s','%s',%d,'%s',%d,%d,'%s','%s'),", item["date"], item["star_id"], item["video_id"], item["expire_time"], item["title"], item["thumb"], item["url"], item["play_count"], item["play_time"], item["source_type"], item["source_name"], item["sort"], item["status"], item["created_at"], item["updated_at"]))
            }
        }

        err = databases.DB.Exec(buffer.String()).Debug().Error

        if err != nil {
            continue
        }

        continue
    }

    return stars, nil
}
    // 獲取代理需要的驗證頭資訊(很多代理都需要驗證,一般按他們的規則要麼把驗證資訊寫到頭資訊,要麼寫到代理地址的後面?&拼接引數)
    func (spider *Spider) GetAgentQueryString() (string, error) {
        agentAppKey := config.AGENT_APP_KEY
        agentAppSecret := config.AGENT_APP_SECRET

        // 建立代理需要的字典
        agentParams := map[string]string{
            "timestamp" : time.Now().Format("2006-01-02 15:04:05"),
            "app_key" : agentAppKey,
        }

        // 對欄位排序
        var keys []string
        for i, _ := range agentParams {
            keys = append(keys, i)
        }

        sort.Strings(keys)
        if len(keys) == 0 {
            return "", nil
        }

        queryString := "MYH-AUTH-MD5 "
        codeString := agentAppSecret
        for i, _ := range keys {
            codeString = codeString + keys[i] + agentParams[keys[i]]
            queryString = queryString + keys[i] + "=" + agentParams[keys[i]] + "&"
        }

        // 獲取Md5的串
        codeString = codeString + agentAppSecret
        md5String := fmt.Sprintf("%x", md5.Sum([]byte(codeString)))

        // 獲取最後的請求頭字串
        queryString = queryString + "sign=" + strings.ToUpper(md5String)

        return queryString, nil
    }

用的GIN框架, 目前是用postman發請求爬,後面我會學習加入事務控制,定時任務,多併發,以及加代理等完善這個爬蟲(目前這些還不會 = =!,望大家多多指教~)。

本地環境,POSTMAN上看,爬50個明星每個明星15條資料,需要59-63秒。(加了爬到每條記錄的時候資料庫檢驗去重還開了SQL語句的debug,去掉這些的話會快些)。

最後貼一些圖片:

Go

爬爬爬爬爬某企鵝的影片

爬爬爬爬爬某企鵝的影片,加了代理和定時任務

爬爬爬爬爬某企鵝的影片,加了代理和定時任務

補定時任務:
用的 github.com/robfig/cron 這個包,可以執行,程式碼如下, 但是總覺得不安全不健壯:

    func main() {
       defer databases.DB.Close()

       // 定時任務
      c := cron.New()
       _ = c.AddFunc("0 0 0/2 * * ?", controllers.VideoSpiderSchedule)
       c.Start()
       //select {
     //}
      router.InitRouter()
    }

心裡比較疑惑,有幾個問題想請教:

1.當我寫成下面形式的時候,定時任務不執行,想知道為什麼?

    func main() {
        defer databases.DB.Close()
        router.InitRouter()
        // 定時任務
        c := cron.New()
        _ = c.AddFunc("0 0 0/2 * * ?", controllers.VideoSpiderSchedule)
        c.Start()
        //select {
        //}
    }
  1. select {} 如果開啟了,下面的 router 就不執行了,想知道 select{} 是幹啥用的?

     func main() {
         defer databases.DB.Close()
    
         // 定時任務
         c := cron.New()
         _ = c.AddFunc("0 0 0/2 * * ?", controllers.VideoSpiderSchedule)
         c.Start()
         select {
         }
         // 開了select 下面的router就不執行了
         router.InitRouter()
     }

3.如果select{} 必須寫, 要怎樣才能既寫 select{} 又能讓 router 不失效,還要讓定時任務執行起來啊?

4.如何才能寫出健壯安全的定時任務啊,求個好的專案和思路參考下下?

初學GO,求大家多多指教,多多幫助哈

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章