13

3行写爬虫 - 使用 Goribot 快速构建 Golang 爬虫

 4 years ago
source link: https://www.tuicool.com/articles/iqqEru3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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 执行的基本单位是 TaskTask 是一个回调函数和请求参数的包装。 s.NewTask() 创建了一个 Task 并作为种子地址添加到任务队列里。

type Task struct {
    Request        *Request
    onRespHandlers []func(ctx *Context)
    Meta           map[string]interface{}
}

Spider 有一个 ThreadPoolSize 参数,大意是 Spider 会根据创建一个虚拟线程池,也就是维护 ThreadPoolSizegoroutine

每个 goroutine 都会从创建开始依次执行 获取新的 Task ->发送网络请求并获取 Response ->顺序执行 Task 里的回调函数(也就是 onRespHandlers )->收集Context中新的 TaskItem ->结束。

关于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 ,如果成功的话就可以通过 HtmlJson 参数获取到。

像之前使用 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.NewTaskWithMetactx.NewTaskWithMeta ,由此可以设置创建的 TaskMeta 数据,即一个 map[string]interface{} 字典。之后在任务执行过程中创建的 Context 也会携带这个 Meta 参数,以此作为新老 Task 之间的数据传递。

ContextMeta 参数同时可以用作数个回调函数和钩子函数之间的数据传递。

钩子函数 与 扩展插件

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 - 仰望星空 如若信息变动请以链接内版本为准。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK