新手寫的視訊爬蟲

賭賭賭賭賭賭賭賭賭聖!發表於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),將上一步拿到的VID帶入進去,拿到視訊詳情(jsonp格式資料:包含視訊圖片,播放地址,播放量,播放時長等等資訊)。
4.用GO的字串擷取把jsonp擷取為json資料,使用 json.Unmarshal 解析到提前準備好的STRUCT 上面。
5.定義切片,把拿到的一個個視訊詳情append到切片中。
6.遍歷切片,資料寫入到資料庫(由於GORM不支援批量寫入,得手動拼接SQL寫入資料庫)。
7.肉眼觀察GO和PHP爬相同的視訊所花的時間。

開始:

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"
)

type SpiderStar struct {
    Status string   `json:"status"`
    Info   string   `json:"info"`
    Data   DataList `json:"data"`
}

type DataList struct {
    List []Result `json:"list"`
}

type Result struct {
    Id     int    `json:"id"`
    Name   string `json:"name"`
}

type Spider struct {
    DataType string
}

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() ([]map[string]interface{}, error) {
    // 這裡是找的一個明星資料的介面,後面某視訊網站需要用到明星的名字
    // 傳送請求,拿到明星的json資料,對映到struct中
    starUrl := "https://XXXXXX/S/dex?page=1&pagesize=10"

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

    if err != nil {
        log.Printf("http.NewRequest star err:%v", 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萬+",
    }

    // 根據明星的名字開始爬視訊,需要urlencode(明星名字)
    var allData []map[string]interface{}
    for _, item := range spiderStar.Data.List {
        spiderListUrl := "https://xxxx.com/s.html?act=102&keyWord=" + url.QueryEscape(item.Name)
        req, err := http.NewRequest("GET", spiderListUrl, nil)

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

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

        document, err := goquery.NewDocumentFromReader(resp.Body)
        if err != nil {
            fmt.Println("抓取" + spider.DataType + "失敗")
            return []map[string]interface{}{}, nil
        }

        // 遍歷標籤,獲取視訊ID
        document.Find(".search_item_h").EachWithBreak(func(i int, selection *goquery.Selection) bool {
            urlString, boolUrl := selection.Find("a").Attr("href")

            if boolUrl {
                // 字串擷取,獲取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
                }

                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
                }

                // 去掉不合法的視訊
                title := videoDetail.Vl.Vi[0].Ti
                if strings.Contains(title, "抖音") ||
                    strings.Contains(title, "douyin") ||
                    strings.Contains(title, "抽菸") ||
                    strings.Contains(title, "整容") ||
                    strings.Contains(title, "戀情") ||
                    strings.Contains(title, "變形計") ||
                    strings.Contains(title, "孟子義") ||
                    strings.Contains(title, "袁冰妍") ||
                    strings.Contains(title, "吸菸") {

                    return false
                }

                // 拿到視訊詳情
                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":     item.Id,
                    "play_time":   strconv.Itoa(videoDetail.Preview),
                    "play_count":  playCount[rand.Intn(3)],
                    "source_type": 1,
                    "source_name": "XX視訊",
                    "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"),
                })

                // 拼接字串,批量寫入
                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 {
                    return false
                }

                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 {
                    return false
                }
            }
            return true;
        })
    }

    return allData, nil
}

注:很多url地址我用XXX代替了…。
我是用gin寫的, 自己建router+controller+models,上面是models裡面的程式碼。router裡面寫的一個路由呼叫這個爬蟲,方便用postman除錯,後面我會學習用定時任務執行以及加代理(目前還不會 = =!,還望大佬指教~)。

本地環境,肉眼看用PHP爬並寫入4000條視訊要11秒左右。
用GO需要9秒左右。

最後貼一個爬到的視訊圖片:

Go

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

相關文章