Golang 爬虫快速入门 | 获取 B 站全站的视频数据

原文首发并持续更新于 https://imagician.net/archives/92/,欲了解更多信息可以前往我的博客 https://imagician.net/

提到爬虫,总会联想到 Python。似乎 Python 是爬虫的唯一选择。爬虫只是完成一个访问页面然后收集数据的任务,用任何语言来写都能实现。相比较 Python 快速实现但是庞大的体型,Golang 来写爬虫似乎是更好的又一选择。

HTTP 请求

Golang 语言的 HTTP 请求库不需要使用第三方的库,标准库就内置了足够好的支持:

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func fetch(url string) string {
	fmt.Println("Fetch Url", url)

	// 创建请求
	req, _ := http.NewRequest("GET", url, nil)
	// 创建HTTP客户端
	client := &http.Client{}
	// 发出请求
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Http get err:", err)
		return ""
	}
	if resp.StatusCode != 200 {
		fmt.Println("Http status code:", resp.StatusCode)
		return ""
	}
	// 读取HTTP响应正文
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Read error", err)
		return ""
	}
	return string(body)
}

func main() {
	fmt.Println(fetch("https://github.com"))
}

使用官方的 HTTP 包可以快速的请求页面并得到返回数据。

就像 Python 有 Scrapy 库,爬虫框架可以很大程度上简化 HTTP 请求、数据抽取、收集的流程,同时还能提供更多的工具来帮助我们实现更复杂的功能。

Golang 爬虫框架——Goribot

https://github.com/zhshch2002/goribot 是一个用 Golang 写成的爬虫轻量框架,有不错的扩展性和分布式支持能力,文档在 https://imagician.net/goribot/

获取 Goribot:

go get -u github.com/zhshch2002/goribot

使用 Goribot 实现上文的代码的功能要看起来简洁不少。

package main

import (
	"fmt"
	"github.com/zhshch2002/goribot"
)

func main() {
	s := goribot.NewSpider()
	s.AddTask(
		goribot.GetReq("https://github.com"),
		func(ctx *goribot.Context) {
			fmt.Println(ctx.Resp.Text)
		},
	)
	s.Run()
}

如此之实现了一个单一的功能,即访问“https://github.com”并打印出结果。如此的应用还不足以使用框架。那我们来入手一个更复杂点的爬虫应用。

用 Goribot 爬取 B 站信息

我们来建立一个复杂点的爬虫应用,预期实现两个功能:

  1. 沿着链接自动发现新的视频链接
  2. 提取标题、封面图、作者和视频数据(播放量、投币、收藏等)

研究 B 站页面

首先我们来研究一下 B 站的视频页面,以 https://www.bilibili.com/video/BV1JE411P7hK?spm_id_from=333.851.b_62696c695f7265706f72745f6469676974616c.21 为例,按 F12 打开调试界面,切换到 Network(网络)选项卡。

image我们能看到这一页面所涉及的所有请求、资源。在调试界面里选在 XHR 选项,来查看 AJAX 请求。

你可以通过点选不同的请求,在右侧弹出的面板里查看具体内容。在新面板里点击 Preview(预览)可以查看服务器响应的内容。

那么,交给你一个任务,依次查看 XHR 下的所有请求,找到最像是服务器返回的点赞、收藏、播放量数据的哪一个。


很好,那来看看你找到是这个吗?

image

你已经成功达成了一个爬虫工程师的成就——从 AJAX 请求里寻找目标数据。

那我们切换到 Header(标头)选项,来看看这个请求对应的参数,最好能找到这个响应和视频 Id 的关系。

image

发现了视频 Id——BV 号。

我们以及解决了核心问题,获取 B 站的视频数据,对于自动搜寻视频,我们可以设定一个起始链接,然后搜寻 <a> 标签来延伸爬取。

搭建爬虫

完整代码在后文。

创建爬虫

s := goribot.NewSpider( // 创建一个爬虫并注册扩展
	goribot.Limiter(true, &goribot.LimitRule{ // 添加一个限制器,限制白名单域名和请求速录限制
		Glob: "*.bilibili.com", // 以防对服务器造成过大压力以及被B站服务器封禁
		Rate: 2,
	}),
	goribot.RefererFiller(),     // 自动填写Referer,参见Goribot(https://imagician.net/goribot/)关于扩展的部分
	goribot.RandomUserAgent(),   // 随机UA
	goribot.SetDepthFirst(true), // 使用深度优先策略,就是沿着一个页面,然后去子页面而非同级页面
)

获取视频数据

var getVideoInfo = func(ctx *goribot.Context) {
	res := map[string]interface{}{
		"bvid":  ctx.Resp.Json("data.bvid").String(),
		"title": ctx.Resp.Json("data.title").String(),
		"des":   ctx.Resp.Json("data.des").String(),
		"pic":   ctx.Resp.Json("data.pic").String(),   // 封面图
		"tname": ctx.Resp.Json("data.tname").String(), // 分类名
		"owner": map[string]interface{}{ //视频作者
			"name": ctx.Resp.Json("data.owner.name").String(),
			"mid":  ctx.Resp.Json("data.owner.mid").String(),
			"face": ctx.Resp.Json("data.owner.face").String(), // 头像
		},
		"ctime":   ctx.Resp.Json("data.ctime").String(),   // 创建时间
		"pubdate": ctx.Resp.Json("data.pubdate").String(), // 发布时间
		"stat": map[string]interface{}{ // 视频数据
			"view":     ctx.Resp.Json("data.stat.view").Int(),
			"danmaku":  ctx.Resp.Json("data.stat.danmaku").Int(),
			"reply":    ctx.Resp.Json("data.stat.reply").Int(),
			"favorite": ctx.Resp.Json("data.stat.favorite").Int(),
			"coin":     ctx.Resp.Json("data.stat.coin").Int(),
			"share":    ctx.Resp.Json("data.stat.share").Int(),
			"like":     ctx.Resp.Json("data.stat.like").Int(),
			"dislike":  ctx.Resp.Json("data.stat.dislike").Int(),
		},
	}
	ctx.AddItem(res) // 保存到蜘蛛的Item处理队列
}

这是一个函数,自动解析响应里的 JSON 数据,也就是刚才看的 AJAX 结果。解析完数据后保存到蜘蛛的 Item 处理队列。

发现新视频

var findVideo goribot.CtxHandlerFun
findVideo = func(ctx *goribot.Context) {
	u := ctx.Req.URL.String()
	fmt.Println(u)
	if strings.HasPrefix(u, "https://www.bilibili.com/video/") { // 判断是否为视频页面
		if strings.Contains(u, "?") {
			u = u[:strings.Index(u, "?")]
		}
		u = u[31:] // 截取视频中的BV号
		fmt.Println(u)

		// 创建一个从BV号获取具体数据的任务,使用上一个策略
		ctx.AddTask(goribot.GetReq("https://api.bilibili.com/x/web-interface/view?bvid="+u), getVideoInfo)
	}
	ctx.Resp.Dom.Find("a[href]").Each(func(i int, sel *goquery.Selection) {
		if h, ok := sel.Attr("href"); ok {
			ctx.AddTask(goribot.GetReq(h), findVideo) // 用同样的策略处理子页面
		}
	})
}

收集 Item

我们在获取视频数据里获取了 AJAX 数据,并保存到 Item 队列。我们在这里处理这些 Item 以避免读写文件和数据库对爬取主线程的阻塞。

s.OnItem(func(i interface{}) interface{} {
	fmt.Println(i) // 我们暂时不做处理,就先打印出来
	return i
})

OnItem 的具体使用要参考 Goribot 文档的相关内容。

最后 Run 吧

// 种子任务
s.AddTask(goribot.GetReq("https://www.bilibili.com/video/BV1at411a7RS"), findVideo)
s.Run()

完整代码如下

package main

import (
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"github.com/zhshch2002/goribot"
	"strings"
)

func main() {
	s := goribot.NewSpider(
		goribot.Limiter(true, &goribot.LimitRule{
			Glob: "*.bilibili.com",
			Rate: 2,
		}),
		goribot.RefererFiller(),
		goribot.RandomUserAgent(),
		goribot.SetDepthFirst(true),
	)
	var getVideoInfo = func(ctx *goribot.Context) {
		res := map[string]interface{}{
			"bvid":  ctx.Resp.Json("data.bvid").String(),
			"title": ctx.Resp.Json("data.title").String(),
			"des":   ctx.Resp.Json("data.des").String(),
			"pic":   ctx.Resp.Json("data.pic").String(),   // 封面图
			"tname": ctx.Resp.Json("data.tname").String(), // 分类名
			"owner": map[string]interface{}{ //视频作者
				"name": ctx.Resp.Json("data.owner.name").String(),
				"mid":  ctx.Resp.Json("data.owner.mid").String(),
				"face": ctx.Resp.Json("data.owner.face").String(), // 头像
			},
			"ctime":   ctx.Resp.Json("data.ctime").String(),   // 创建时间
			"pubdate": ctx.Resp.Json("data.pubdate").String(), // 发布时间
			"stat": map[string]interface{}{ // 视频数据
				"view":     ctx.Resp.Json("data.stat.view").Int(),
				"danmaku":  ctx.Resp.Json("data.stat.danmaku").Int(),
				"reply":    ctx.Resp.Json("data.stat.reply").Int(),
				"favorite": ctx.Resp.Json("data.stat.favorite").Int(),
				"coin":     ctx.Resp.Json("data.stat.coin").Int(),
				"share":    ctx.Resp.Json("data.stat.share").Int(),
				"like":     ctx.Resp.Json("data.stat.like").Int(),
				"dislike":  ctx.Resp.Json("data.stat.dislike").Int(),
			},
		}
		ctx.AddItem(res)
	}
	var findVideo goribot.CtxHandlerFun
	findVideo = func(ctx *goribot.Context) {
		u := ctx.Req.URL.String()
		fmt.Println(u)
		if strings.HasPrefix(u, "https://www.bilibili.com/video/") {
			if strings.Contains(u, "?") {
				u = u[:strings.Index(u, "?")]
			}
			u = u[31:]
			fmt.Println(u)
			ctx.AddTask(goribot.GetReq("https://api.bilibili.com/x/web-interface/view?bvid="+u), getVideoInfo)
		}
		ctx.Resp.Dom.Find("a[href]").Each(func(i int, sel *goquery.Selection) {
			if h, ok := sel.Attr("href"); ok {
				ctx.AddTask(goribot.GetReq(h), findVideo)
			}
		})
	}
	s.OnItem(func(i interface{}) interface{} {
		fmt.Println(i)
		return i
	})
	s.AddTask(goribot.GetReq("https://www.bilibili.com/video/BV1at411a7RS").SetHeader("cookie", "_uuid=1B9F036F-8652-DCDD-D67E-54603D58A9B904750infoc; buvid3=5D62519D-8AB5-449B-A4CF-72D17C3DFB87155806infoc; sid=9h5nzg2a; LIVE_BUVID=AUTO7815811574205505; CURRENT_FNVAL=16; im_notify_type_403928979=0; rpdid=|(k|~uu|lu||0J'ul)ukk)~kY; _ga=GA1.2.533428114.1584175871; PVID=1; DedeUserID=403928979; DedeUserID__ckMd5=08363945687b3545; SESSDATA=b4f022fe%2C1601298276%2C1cf0c*41; bili_jct=2f00b7d205a97aa2ec1475f93bfcb1a3; bp_t_offset_403928979=375484225910036050"), findVideo)
	s.Run()
}

最后

爬虫框架只是工具,重要的是人怎么使用它。了解工具可以看项目_examples文档

  • 爬虫

    网络爬虫(Spider、Crawler),是一种按照一定的规则,自动地抓取万维网信息的程序。

    86 引用 • 192 回帖 • 2 关注
  • 哔哩哔哩
    17 引用 • 132 回帖 • 2 关注
  • 创造

    你创造的作品可能会帮助到很多人,如果是开源项目的话就更赞了!

    99 引用 • 619 回帖
  • golang

    Go 语言是 Google 推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去 10 多年间软件开发的难度令人沮丧。Go 是谷歌 2009 发布的第二款编程语言。

    404 引用 • 1300 回帖 • 689 关注
回帖
请输入回帖内容...