Loading... <div class="tip share">请注意,本文编写于 474 天前,最后修改于 81 天前,其中某些信息可能已经过时。</div> <div class="tip inlineBlock error"> Goribot已经被迁移到[Gospider](https://github.com/zhshch2002/gospider),解决了之前设计上的一些问题,同时使用方式基本不变 </div> [zhshch2002/goribot: [Crawler/Scraper for Golang]Make a Golang spider in 3 lines](https://github.com/zhshch2002/goribot)是我的一个业余项目,目的是能尽可能简洁的使用Golang开发爬虫应用。 **注意:这个项目正处于beta版本,不建议直接使用在重要项目上。Goribot的功能都经过测试,如果有问题欢迎来提issues。** # 安装 ```shell go get -u github.com/zhshch2002/goribot ``` # 访问网络 不需要冗长的初始化和配置过程,使用 `goribot`的基本功能只需要三步。 ```Go 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`并作为种子地址添加到任务队列里。 ```Go 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`,这是对网络响应数据和一些操作的包装。 ```Go 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`里保存的新任务收集起来添加到队列里。 ```go 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`提供一系列钩子函数的挂载点,可以在一个任务执行的不同时间进行处理。 ```Go 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`扩展源码如下。 ```Go // 使用时可以调用 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`钩子函数。 ```Go 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() ``` # 复杂一些的例子——哔哩哔哩爬虫 这是一个用于爬取哔哩哔哩视频的蜘蛛。 ```go 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() } ``` Last modification:November 8th, 2020 at 05:14 pm © 允许规范转载