关于使用colly爬取b站评论进行输出的实现 | 青训营

209 阅读7分钟

Colly是Golang世界最知名的Web爬虫框架了,它的API清晰明了,高度可配置和可扩展,支持分布式抓取,还支持多种存储后端(如内存、Redis、MongoDB等)。这篇文章记录我使用colly在爬取b站评论时的整体过程 :)

!!!这并不是一篇教程,勉强说只是一个使用colly的小例子,只是记录了我写代码的过程而已,不会对具体的代码或方法进行具体说明,所以如果您还没有学习colly,建议您先去学一下,上手使用一下

思路:
  1.         获取b站某个视频评论数据,需要找到具体的url,数据会以json格式被url返回
  2.         拥有相对应的结构体,评论中的数据可以放在结构体当中
  3.         获取请求头  
  4.         使用colly的OnResponse()方法进行对得到的json数据进行处理

~~ ~~

首先,网页可以分为静态网页和动态网页,对于静态网页,如下图bing搜索页面,可以直接在开发者工具中的网络项里很容易找到该页面的url,而该页面的所有数据都存放在该url当中。

20419dc43d72febf20780d0fc64b71bcb1b96d47.png@1256w_750h_!web-article-pic.webp 因此只需要在元素项当中找到想要爬取的内容复制其selector选择器,这一步也很简单,然后在代码中使用OnHTML()函数进行操作,b站也有相关的视频对此讲解,因此不再累述。

8a41c8040b9cf9911d2a59a3f0945bf4eb152f7c.png@1256w_754h_!web-article-pic.webp 而对于像哔哩哔哩这样的动态网页,我们无法使用OnHTML直接爬取整个页面的数据,因为所有数据并不会放在该页面地址栏上的url里,比如b站评论的数据会单独放在一个url里进行返回。

所以首先需要在开发者工具里找到属于评论数据的url,一般为json格式,在网络项中选中Fetch/XHR格式,若名称条数依旧很多,可以在筛选器中输入reply进行筛选

f3a952e39f63939e2fbceb333a70e6a2ce1b911c.png@1256w_752h_!web-article-pic.webp 在预览中我们可以看到返回的评论数据

0953b8bd90373440cd8daa048887e5e28c691087.png@1256w_250h_!web-article-pic.webp 在标头中可以打开该url进行查看

5f81a06dce3b586e3b568728762102ef23b74c5c.png@1256w_198h_!web-article-pic.webp 这密密麻麻的数据真是闪我一脸,想要更好的查看返回的json数据可以在edge浏览器安装 JSON-handle插件

b49a7e5ee2b36dfcfa9b279a30d244b02a921cf1.png@1256w_754h_!web-article-pic.webp

6ce378162c0b1868cd2c90e5e74eefad437137f0.png@1256w_554h_!web-article-pic.webp 安装好后再次打开url,啊,清爽多了

发现该url只记录了前20个评论数据,可以推出还有其他的url,因为是GET请求,所以通过比较不同其它评论的url,可以通过更改param获取不同的20条评论,这件事若读者有兴趣可以去做一下,在此我不做深究

b2f99f5a287b139be8adfe53abef1ab2188b5a36.png@1256w_750h_!web-article-pic.webp 然后我们就需要把这些json数据转化为对应的结构体,这样url返回的数据就可以直接存储在我们定义的结构体里了,为了简便我们可以使用在线工具进行转换

这里提供一个网站 JSON转Golang Struct - 在线工具 - OKTools oktools.net/json2go

6273a19f75e57d8f89725797bd11332bd3d28140.png@1256w_756h_!web-article-pic.webp

可以根据喜好选择结构体展开或嵌套的形式,我这边选择嵌套,看着更直观一点

复制后把代码放到IDE中,结构体竟然达到了快500行,并且有错误

d0a36965016dbbabf84bccf1871d5f63d8d71082.png@!web-article-pic.webp 查看后错误是命名重复导致的,先无视

首先注意到生成的结构体的变量名称是以驼峰名命名的,对应着json数据的蛇形命名

98eabb7372f4cd22ee87564d1cbc3e772aafb03f.png@1256w_942h_!web-article-pic.webp 对json数据变量名称一一对比,确实只是多了3个Content结构体,删除即可,而对我们不需要的数据,也可以在结构体中进行删除

75353f9fcb8b70ef446c3304b6f27a707fbb856c.png@1256w_1072h_!web-article-pic.webp 最终,我留下了以下结构体数据,如下图

5050ce4252ff97eb23a5364ac758f807bc7f937c.png@1256w_1002h_!web-article-pic.webp

type ReplyContainer struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
	TTL     int    `json:"ttl"`
	Data    struct {
		Replies []struct { //评论
			Member struct { //评论用户
				Mid   string `json:"mid"`   //用户id
				Uname string `json:"uname"` //用户姓名
				Sex   string `json:"sex"`   //性别
			} `json:"member"`
			Content struct {
				Message string        `json:"message"` //评论内容
				Members []interface{} `json:"members"`
				MaxLine int           `json:"max_line"`
			} `json:"content,omitempty"`
			ReplyControl struct {
				MaxLine           int    `json:"max_line"`
				SubReplyEntryText string `json:"sub_reply_entry_text"`
				SubReplyTitleText string `json:"sub_reply_title_text"`
				TimeDesc          string `json:"time_desc"` //评论发布时间
			} `json:"reply_control"`
		} `json:"replies"`
	} `json:"data"`
}

现在我们的结构体有了,对应的url也同样找到了,我们还需要再网页上复制该网页的请求标头

4a3ba9b01f41f7a64126b2d83e43b93f104e4481.png@1256w_756h_!web-article-pic.webp 当然,我们可以使用在线工具自动生成请求标头的相关代码,

我分享一个我使用的:在线curl命令转代码 (lddgo.net)

www.lddgo.net/convert/cur…

怎么使用呢? 如下图操作,复制为cURL(bash),不用全部复制哦

0bad45be449dc73c278c4a6356bef8b88c013303.png@1256w_366h_!web-article-pic.webp 我们这个的url请求方式是GET,所以选择GET,把复制内容覆盖进去进行转换

f28010821259aa95e346caf9654102658ab0094f.png@1256w_754h_!web-article-pic.webp 输出结果是一个go原生的连接url的代码,我们只需要复制所有的req.Header.Set()即可,

*如果同一个网站被相同浏览器频繁访问,很容易被网站识别为爬虫程序,所以一般通过使用多个User-Agent随机调用的方式,可以有效避免同一个请求头访问网站,网上也有很多的方式去随机生成user-agent,这边我找了一个生成随机数的方法

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// RandomString 生成一个随机的user-agent
func RandomString() string {
	b := make([]byte, rand.Intn(10)+10)
	for i := range b {
		b[i] = letterBytes[rand.Intn(len(letterBytes))]
	}
	return string(b)
} 

现在我们的代码应该是这个样子

3b33707bb6e0fce3e2a316bb42b4e47bb4075fc1.png@1256w_758h_!web-article-pic.webp 然后我们在使用On.Response()函数对得到的json数据进行处理

47bca7c6f7d83829ca7acda58cded96fd0490a76.png@1256w_344h_!web-article-pic.webp 现在我们就可以进行遍历输出了

f84c3c16ee2522dda29da596d55dd0ca13d46040.png@1256w_124h_!web-article-pic.webp

cfcee5425947b4fc362a066af6a16140bbb2e3e2.png@1256w_732h_!web-article-pic.webp 嗯...评论太长了,我们换一个视频并按时间对评论进行排序

c5c8d8a3abf0acf03d77b3e77cefeb2bcf1f0db8.png@1256w_738h_!web-article-pic.webp

可以清楚看到共20条,大功告成!!!

139014b9c8dd4c3d1cb4a3d06aabfb8630244501.png@1256w_754h_!web-article-pic.webp 附上全部代码

package main

import (
	"encoding/json"
	"fmt"
	"github.com/gocolly/colly"
	"log"
	"math/rand"
)

type ReplyContainer struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
	TTL     int    `json:"ttl"`
	Data    struct {
		Replies []struct { //评论
			Member struct { //评论用户
				Mid   string `json:"mid"`   //用户id
				Uname string `json:"uname"` //用户姓名
				Sex   string `json:"sex"`   //性别
			} `json:"member"`
			Content struct {
				Message string        `json:"message"` //评论内容
				Members []interface{} `json:"members"`
				MaxLine int           `json:"max_line"`
			} `json:"content,omitempty"`
			ReplyControl struct {
				MaxLine           int    `json:"max_line"`
				SubReplyEntryText string `json:"sub_reply_entry_text"`
				SubReplyTitleText string `json:"sub_reply_title_text"`
				TimeDesc          string `json:"time_desc"` //评论发布时间
			} `json:"reply_control"`
		} `json:"replies"`
	} `json:"data"`
}

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

// RandomString 生成一个随机的user-agent
func RandomString() string {
	b := make([]byte, rand.Intn(10)+10)
	for i := range b {
		b[i] = letterBytes[rand.Intn(len(letterBytes))]
	}
	return string(b)
}

func main() {
	c := colly.NewCollector()

	//设置请求头
	c.OnRequest(func(req *colly.Request) {
		req.Headers.Set("authority", "api.bilibili.com")
		req.Headers.Set("accept", "application/json, text/plain, */*")
		req.Headers.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
		req.Headers.Set("cookie", "buvid3=17AE2C8A-B723-FCFD-CA24-BB42886ADC1720800infoc; b_nut=1676006620; i-wanna-go-back=-1; _uuid=AD1102BD9-4377-AEEC-2CDC-10BFF532103E1C20715infoc; DedeUserID=347658083; DedeUserID__ckMd5=c15196b1de6416c6; rpdid=|(JY)RJu)JRJ0J'uY~Y||Ym|); b_ut=5; nostalgia_conf=-1; buvid4=6CEBCDF6-C2BC-E661-A89D-EF5488939A6D21670-023021013-FmUiCC4TrOvqFWiIDh%2F07Q%3D%3D; LIVE_BUVID=AUTO2916763501096807; is-2022-channel=1; hit-dyn-v2=1; blackside_state=0; CURRENT_BLACKGAP=0; SESSDATA=9e31d506%2C1694475764%2Ca17c3%2A32; bili_jct=2318dd41406535428217db54bc617364; CURRENT_PID=982e3db0-c974-11ed-9abb-ab8a696d4338; hit-new-style-dyn=1; buvid_fp_plain=undefined; home_feed_column=4; browser_resolution=1280-649; header_theme_version=CLOSE; FEED_LIVE_VERSION=V8; i-wanna-go-feeds=-1; CURRENT_FNVAL=4048; bili_ticket=eyJhbGciOiJFUzM4NCIsImtpZCI6ImVjMDIiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2OTE5ODczMzgsImlhdCI6MTY5MTcyODEzOCwicGx0IjotMX0.IFuNJxp7-eYGGucUiHOGki302PJCOy_DTDnGst8CU22PgItGELCknDv-ttY2W-FcnLZkNlf3lyFSCck9GygWuPLa_sQx5pOXNIdQjUk_pvjyMIwXJvUh7hUBlnhm-dMO; bili_ticket_expires=1691987338; fingerprint=1901b21587cf76da1fcebf71b774fb7a; buvid_fp=30e4f3affecaec8758b47d527c385458; sid=5ng2rgm3; CURRENT_QUALITY=80; b_lsid=7A87A66F_189EDBBC802; bp_video_offset_347658083=829243419043823638; PVID=9")
		req.Headers.Set("origin", "https://www.bilibili.com")
		req.Headers.Set("referer", "https://www.bilibili.com/bangumi/play/ep327584?spm_id_from=333.337.0.0&from_spmid=666.25.episode.0")
		req.Headers.Set("sec-ch-ua", `"Not/A)Brand";v="99", "Microsoft Edge";v="115", "Chromium";v="115"`)
		req.Headers.Set("sec-ch-ua-mobile", "?0")
		req.Headers.Set("sec-ch-ua-platform", `"Windows"`)
		req.Headers.Set("sec-fetch-dest", "empty")
		req.Headers.Set("sec-fetch-mode", "cors")
		req.Headers.Set("sec-fetch-site", "same-site")
		req.Headers.Set("user-agent", RandomString())
	})
	//结构体 用来存放评论数据
	container := ReplyContainer{}
	//c := colly.NewCollector() c是怎么来的
	c.OnResponse(func(r *colly.Response) {
		fmt.Println("response received", r.StatusCode) //打印状态码 成功访问为200
		err := json.Unmarshal(r.Body, &container)      //r.Body为得到的json数据 []byte类型 进行反序列化 字节序列转化成对象
		if err != nil {
			fmt.Println("error", err)
			log.Fatal(err)
		}
	})
	//访问url
	c.Visit("https://api.bilibili.com/x/v2/reply/main?csrf=2318dd41406535428217db54bc617364&mode=2&oid=455975442&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&type=1")

	for i, reply := range container.Data.Replies {
		fmt.Println(i, "姓名", reply.Member.Uname, "内容", reply.Content.Message, reply.ReplyControl.TimeDesc)
	}
}