今天心血來潮,想用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,去掉這些的話會快些)。
最後貼一些圖片:
補定時任務:
用的 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 {
//}
}
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 協議》,轉載必須註明作者和本文連結