开篇:Go简单爬虫框架GCrawler

247 阅读3分钟

GCrawler爬虫框架

切记:本框架仅适用于学习目的不具有生产级代码质量

一、介绍(废话)

什么是爬虫框架?

爬虫框架是指为了简化和加速网络爬虫(Web Crawler)开发而设计的一种软件框架或库。网络爬虫是一类用于自动抓取互联网上信息的程序,常用于数据采集、搜索引擎建设、监测和分析等领域。

为什么需要爬虫框架?

简单来说,框架提供了一系列已经封装好的功能和工具,使开发者可以更专注于业务逻辑和数据处理,而不必过多关注底层网络请求和数据解析的细节,从而更快地构建爬虫程序。

为什么要从0自己实现?

没什么,就是玩。并且想要自己更方便的定制一些需要的功能。

GCrawler是什么?

如上,是一个简单的爬虫框架(极大程度上参考/抄了Crawlee框架),仅仅是为了简化个人开发。

二、功能

数据抓取(不支持JS处理)

只需要调用AddReqAddReqs,框架会自动将爬取任务加入内部队列。可以通过WithLabel来指定该请求的处理器,未指定则默认采用default处理器。

s.AddReq("https://cs.mnnu.edu.cn/xwgg.htm").WithLabel("default")
// 或
s.AddReqs([]string{
    "https://a.com",
    "https://b.com",
    "https://c.com",
})

自定义处理器

通过SetDefaultHandler可以指定请求的默认处理器,如果请求未指定处理器,则会默认使用此处理器。

s.SetDefaultHandler(func(s *spider.Spider, ctx *spider.TaskContext, d *goquery.Document) (interface{}, error) {
    log.Info().Msgf("default callback: %s", ctx.GetUrl())
    d.Find(`tr[id^='line_u7_'] table a`).Each(func(i int, doc *goquery.Selection) {
       t := doc.Parent().Parent().Find("td").Last().Text()
       link := doc.AttrOr("href", "")
       s.AddReq(ctx.MergeUrl(link)).WithLabel("detail").WithExtras(map[string]interface{}{
          "time": t,
       })
    })
    return nil, nil
})

通过AddHandler可以添加特定的命名处理器。

s.AddHandler("detail", func(s *spider.Spider, ctx *spider.TaskContext, d *goquery.Document) (interface{}, error) {
    log.Info().Msgf("detail callback: %s", ctx.GetUrl())
    data := make(map[string]interface{})
    data["title"] = d.Find(".Article_Title").Text()
    h, _ := d.Find("div[id^=vsb_content]").Html()
    data["content"] = h
    data["time"] = ctx.GetExtra("time")

    return data, nil
})

无限分页抓取

在需要处理分页的处理器中,添加如下代码即可。

s.AutoFollowPaginationLinks(ctx, "span[class^='p_no'] a", "href")

数据管道

在处理器中,如果第一个返回值不为nil,则会将其作为数据,丢到持久化处理器链中。并且,如果持久化处理器的第一个返回值不为nil,则会继续向下一个持久化处理器传递。

s.AddPersistHandler(func(s *spider.Spider, ctx *spider.TaskContext, items interface{}) (interface{}, error) {
    o, err := sonic.Marshal(&items)
    if err != nil {
       return nil, err
    }
    log.Info().Msgf("persist callback: %s", string(o))
    return items, nil
})

s.AddPersistHandler(func(s *spider.Spider, ctx *spider.TaskContext, items interface{}) (interface{}, error) {
    // 持久化到数据库等操作
    return nil, nil
})

图片下载

可以指定图片保存路径。

s.SetDefaultImgPath("/temp")

简单调用如下代码,即可抓取指定Document下的所有图片(这是我的需求,不需要精细化控制)。

s.ProcessImageLinks(ctx, d.Find("div[id^=vsb_content]"))

每一个图片下载成功后会执行imageDownloadCallback,可以通过SetImageDownloadCallback指定。这个回调用来进行图片链接的处理,图片下载下来后,可以在此上传到oss,拿到新的图片资源链接。第一个返回值指定新的资源链接,第二个是否执行替换(替换原图片链接)。

s.SetImageDownloadCallback(func(spider *Spider, context *TaskContext, old string, new string) (string, bool, error) {
    return new, true, nil
})

执行报告

通过如下代码获取报告。

report := s.GenerateReport()
println(report)

三、完整示例

func main() {
    log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
    retryOptions := spider.RetryOptions{
       MaxRetries:    5,
       BackoffFactor: 5 * time.Second,
       Timeout:       time.Second * 10,
    }

    s := spider.NewSpider(retryOptions, 30)

    s.SetDefaultImgPath("/img")

    s.SetDefaultHandler(func(s *spider.Spider, ctx *spider.TaskContext, d *goquery.Document) (interface{}, error) {
       log.Info().Msgf("default callback: %s", ctx.GetUrl())
       d.Find(`tr[id^='line_u7_'] table a`).Each(func(i int, doc *goquery.Selection) {
          t := doc.Parent().Parent().Find("td").Last().Text()
          link := doc.AttrOr("href", "")
          s.AddReq(ctx.MergeUrl(link)).WithLabel("detail").WithExtras(map[string]interface{}{
             "time": t,
          })
       })

       s.AutoFollowPaginationLinks(ctx, "span[class^='p_no'] a", "href")

       return nil, nil
    })

    s.AddHandler("detail", func(s *spider.Spider, ctx *spider.TaskContext, d *goquery.Document) (interface{}, error) {
       log.Info().Msgf("detail callback: %s", ctx.GetUrl())

       s.ProcessImageLinks(ctx, d.Find("div[id^=vsb_content]"))

       data := make(map[string]interface{})
       data["title"] = d.Find(".Article_Title").Text()
       h, _ := d.Find("div[id^=vsb_content]").Html()
       data["content"] = h
       data["time"] = ctx.GetExtra("time")

       return data, nil
    })

    s.AddPersistHandler(func(s *spider.Spider, ctx *spider.TaskContext, items interface{}) (interface{}, error) {
       o, err := sonic.Marshal(&items)
       if err != nil {
          return nil, err
       }
       log.Info().Msgf("persist callback: %s", string(o))
       return items, nil
    })

    s.AddPersistHandler(func(s *spider.Spider, ctx *spider.TaskContext, items interface{}) (interface{}, error) {
       // 数据持久化
       return nil, nil
    })

    s.AddReqs([]string{
       "https://example.com/a.htm",
       "https://example.com/b.htm",
       "https://example.com/c.htm",
    })

    s.Start()

    report := s.GenerateReport()
    println(report)

}

四、总结

功能简单,但满足我需求了。后面的篇章会从需求出发,一点点实现这个简单的框架,大约300行代码左右。 Github参考:PLin2023/gcrawler (github.com)