zhshch2002/goribot: [Crawler/Scraper for Golang]Make a Golang spider in 3 lines是我的一个业余项目,目的是能尽可能简洁的使用Golang开发爬虫应用。
注意:这个项目正处于beta版本,不建议直接使用在重要项目上。Goribot的功能都经过测试,如果有问题欢迎来提issues。
安装
go get -u github.com/zhshch2002/goribot
复制代码
访问网络
不需要冗长的初始化和配置过程,使用goribot
的基本功能只需要三步。
package main
import (
"fmt"
"github.com/zhshch2002/goribot"
)
func main() {
s := goribot.NewSpider() // 1 创建蜘蛛
s.NewTask( // 2 添加任务
goribot.MustNewGetReq("https://httpbin.org/get?hello=world"),
func(ctx *goribot.Context) {
fmt.Println("got resp data", ctx.Text)
})
s.Run() // 3 运行
}
复制代码
goribot
执行的基本单位是Task
,Task
是一个回调函数和请求参数的包装。s.NewTask()
创建了一个Task
并作为种子地址添加到任务队列里。
type Task struct {
Request *Request
onRespHandlers []func(ctx *Context)
Meta map[string]interface{}
}
复制代码
Spider
有一个ThreadPoolSize
参数,大意是Spider
会根据创建一个虚拟线程池,也就是维护ThreadPoolSize
个goroutine
。
每个goroutine
都会从创建开始依次执行 获取新的Task
->发送网络请求并获取Response
->顺序执行Task
里的回调函数(也就是onRespHandlers
)->收集Context中新的Task
和Item
->结束。
关于Context
由刚才的例子,回调函数收到的数据是ctx *goribot.Context
,这是对网络响应数据和一些操作的包装。
type Context struct {
Text string // the response text
Html *goquery.Document // spider will try to parse the response as html
Json map[string]interface{} // spider will try to parse the response as json
Request *Request // origin request
Response *Response // a response object
Tasks []*Task // the new request task which will send to the spider
Items []interface{} // the new result data which will send to the spider,use to store
Meta map[string]interface{} // the request task created by NewTaskWithMeta func will have a k-y pair
drop bool // in handlers chain,you can use ctx.Drop() to break the handler chain and stop handling
}
复制代码
在这里蜘蛛会试着把收到的数据转换为字符串也就是Text
属性,之后会试着将其解析为HTML
或者JSON
,如果成功的话就可以通过Html
和Json
参数获取到。
像之前使用spider.NewTask()
向蜘蛛任务队列添加新任务,在回调函数里应该使用ctx.NewTask()
创建新的任务。蜘蛛会在所有回调函数执行结束后将ctx
里保存的新任务收集起来添加到队列里。
s := goribot.NewSpider()
var getNewLinkHandler func(ctx *goribot.Context) // 这样声明的回调函数可以在函数内将自己作为参数
getNewLinkHandler = func(ctx *goribot.Context) {
ctx.Html.Find("a[href]").Each(func(i int, selection *goquery.Selection) {
rawurl, _ := selection.Attr("href")
u, err := ctx.Request.Url.Parse(rawurl)
if err != nil {
return
}
if r, err := goribot.NewGetReq(u.String()); err == nil {
// 在回调函数内创建新的任务
// 并且使用自己作为新任务的回调函数
ctx.NewTask(r, getNewLinkHandler)
}
})
}
// 种子任务
s.NewTask(goribot.MustNewGetReq("https://www.bilibili.com/video/av66703342"), getNewLinkHandler)
s.Run()
复制代码
添加新任务时可以使用spider.NewTaskWithMeta
和ctx.NewTaskWithMeta
,由此可以设置创建的Task
的Meta
数据,即一个map[string]interface{}
字典。之后在任务执行过程中创建的Context
也会携带这个Meta
参数,以此作为新老Task
之间的数据传递。
Context
的Meta
参数同时可以用作数个回调函数和钩子函数之间的数据传递。
钩子函数 与 扩展插件
spider
提供一系列钩子函数的挂载点,可以在一个任务执行的不同时间进行处理。
s := NewSpider()
s.OnTask(func(ctx *goribot.Context, k *goribot.Task) *goribot.Task { // 当有新任务提交的时候执行,可以返回nil来抛弃任务
fmt.Println("on task", k)
return k
})
s.OnResp(func(ctx *goribot.Context) { // 当下载完一个请求后执行的函数,先于Task的回调函数执行
fmt.Println("on resp")
})
s.OnItem(func(ctx *goribot.Context, i interface{}) interface{} { // 当有新结果数据提交的时候执行,用作数据的存储(稍后讲到),可以返回nil来抛弃
fmt.Println("on item", i)
return i
})
s.OnError(func(ctx *goribot.Context, err error) { // 当出现下载器出现错误时执行
fmt.Println("on error", err)
})
复制代码
Tip:这些钩子函数并非是一个而是一列,可以通过多次调用上述函数来设置多个钩子。钩子函数的执行顺序也会按照其被注册的顺序执行。
插件或者叫扩展指的是在执行s := goribot.NewSpider()
时可以传入的一种函数参数。这个函数在创建蜘蛛时被执行,用来配置蜘蛛的参数或者增加钩子函数。例如内建的HostFilter
扩展源码如下。
// 使用时可以调用 s := goribot.NewSpider(HostFilter("www.bilibili.com"))
// 由此创建出的蜘蛛会自动忽略www.bilibili.com以外的链接
func HostFilter(h ...string) func(s *Spider) {
WhiteList := map[string]struct{}{}
for _, i := range h {
WhiteList[i] = struct{}{}
}
return func(s *Spider) {
s.OnTask(func(ctx *Context, k *Task) *Task {
if _, ok := WhiteList[k.Request.Url.Host]; ok {
return k
}
return nil
})
}
}
复制代码
存储
不建议在回调函数内存储数据,所以ctx
提供ctx.AddItem
函数用于添加一些数据到ctx
中保存,执行到最后spider
会收集他们并调用OnItem
钩子函数。
s := goribot.NewSpider()
s.NewTask(goribot.MustNewGetReq("https://httpbin.org/"), func(ctx *goribot.Context) {
ctx.AddItem(ctx.Text)
})
s.OnItem(func(ctx *goribot.Context, i interface{}) interface{} {
fmt.Println("get item", i) // 在此可以统一的对收集到的数据进行存储
return i
})
s.Run()
复制代码
复杂一些的例子——哔哩哔哩爬虫
这是一个用于爬取哔哩哔哩视频的蜘蛛。
package main
import (
"github.com/PuerkitoBio/goquery"
"github.com/zhshch2002/goribot"
"log"
"strings"
)
type BiliVideoItem struct {
Title, Url string
}
func main() {
s := goribot.NewSpider(goribot.HostFilter("www.bilibili.com"), goribot.ReqDeduplicate(), goribot.RandomUserAgent())
var biliVideoHandler, getNewLinkHandler func(ctx *goribot.Context)
// 获取新链接
getNewLinkHandler = func(ctx *goribot.Context) {
ctx.Html.Find("a[href]").Each(func(i int, selection *goquery.Selection) {
rawurl, _ := selection.Attr("href")
if !strings.HasPrefix(rawurl, "/video/av") {
return
}
u, err := ctx.Request.Url.Parse(rawurl)
if err != nil {
return
}
u.RawQuery = ""
if strings.HasSuffix(u.Path, "/") {
u.Path = u.Path[0 : len(u.Path)-1]
}
//log.Println(u.String())
if r, err := goribot.NewGetReq(u.String()); err == nil {
ctx.NewTask(r, getNewLinkHandler, biliVideoHandler)
}
})
}
// 将数据提取出来
biliVideoHandler = func(ctx *goribot.Context) {
ctx.AddItem(BiliVideoItem{
Title: ctx.Html.Find("title").Text(),
Url: ctx.Request.Url.String(),
})
}
// 抓取种子链接
s.NewTask(goribot.MustNewGetReq("https://www.bilibili.com/video/av66703342"), getNewLinkHandler, biliVideoHandler)
s.OnItem(func(ctx *goribot.Context, i interface{}) interface{} {
log.Println(i) // 可以做一些数据存储工作
return i
})
s.Run()
}
复制代码
本文原始发布于 使用 Goribot 快速构建 Golang 爬虫 - AthorX - 仰望星空 如若信息变动请以链接内版本为准。
有疑问加站长微信联系(非本文作者)