爬爬爬爬爬某企鹅的视频,加了代理和定时任务

今天心血来潮,想用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 协议》,转载必须注明作者和本文链接
讨论数量: 8

刚开始学go,不错。学习了

3年前 评论

推荐 github.com/tidwall/gjson 这个包,可以不用定义结构体去接收数据了

3年前 评论

GORM 是支持批量的, 你用的是 1吧

3年前 评论

@Remember 刚看了V2的文档,确实支持的

3年前 评论

select {} 在这里是起阻塞的作用

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!