Colly是Golang世界最知名的Web爬虫框架了,它的API清晰明了,高度可配置和可扩展,支持分布式抓取,还支持多种存储后端(如内存、Redis、MongoDB等)。这篇文章记录我使用colly在爬取b站评论时的整体过程 :)
!!!这并不是一篇教程,勉强说只是一个使用colly的小例子,只是记录了我写代码的过程而已,不会对具体的代码或方法进行具体说明,所以如果您还没有学习colly,建议您先去学一下,上手使用一下
思路:
- 获取b站某个视频评论数据,需要找到具体的url,数据会以json格式被url返回
- 拥有相对应的结构体,评论中的数据可以放在结构体当中
- 获取请求头
- 使用colly的OnResponse()方法进行对得到的json数据进行处理
~~ ~~
首先,网页可以分为静态网页和动态网页,对于静态网页,如下图bing搜索页面,可以直接在开发者工具中的网络项里很容易找到该页面的url,而该页面的所有数据都存放在该url当中。
因此只需要在元素项当中找到想要爬取的内容复制其selector选择器,这一步也很简单,然后在代码中使用OnHTML()函数进行操作,b站也有相关的视频对此讲解,因此不再累述。
而对于像哔哩哔哩这样的动态网页,我们无法使用OnHTML直接爬取整个页面的数据,因为所有数据并不会放在该页面地址栏上的url里,比如b站评论的数据会单独放在一个url里进行返回。
所以首先需要在开发者工具里找到属于评论数据的url,一般为json格式,在网络项中选中Fetch/XHR格式,若名称条数依旧很多,可以在筛选器中输入reply进行筛选
在预览中我们可以看到返回的评论数据
在标头中可以打开该url进行查看
这密密麻麻的数据真是闪我一脸,想要更好的查看返回的json数据可以在edge浏览器安装
JSON-handle插件
安装好后再次打开url,啊,清爽多了
发现该url只记录了前20个评论数据,可以推出还有其他的url,因为是GET请求,所以通过比较不同其它评论的url,可以通过更改param获取不同的20条评论,这件事若读者有兴趣可以去做一下,在此我不做深究
然后我们就需要把这些json数据转化为对应的结构体,这样url返回的数据就可以直接存储在我们定义的结构体里了,为了简便我们可以使用在线工具进行转换
这里提供一个网站 JSON转Golang Struct - 在线工具 - OKTools oktools.net/json2go
可以根据喜好选择结构体展开或嵌套的形式,我这边选择嵌套,看着更直观一点
复制后把代码放到IDE中,结构体竟然达到了快500行,并且有错误
查看后错误是命名重复导致的,先无视
首先注意到生成的结构体的变量名称是以驼峰名命名的,对应着json数据的蛇形命名
对json数据变量名称一一对比,确实只是多了3个Content结构体,删除即可,而对我们不需要的数据,也可以在结构体中进行删除
最终,我留下了以下结构体数据,如下图
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也同样找到了,我们还需要再网页上复制该网页的请求标头
当然,我们可以使用在线工具自动生成请求标头的相关代码,
我分享一个我使用的:在线curl命令转代码 (lddgo.net)
怎么使用呢? 如下图操作,复制为cURL(bash),不用全部复制哦
我们这个的url请求方式是GET,所以选择GET,把复制内容覆盖进去进行转换
输出结果是一个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)
}
现在我们的代码应该是这个样子
然后我们在使用On.Response()函数对得到的json数据进行处理
现在我们就可以进行遍历输出了
嗯...评论太长了,我们换一个视频并按时间对评论进行排序
可以清楚看到共20条,大功告成!!!
附上全部代码
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)
}
}