# Gospider

codecov goproxycn Go Test codecov

  • Gospider - https://github.com/zhshch2002/gospider
  • Goreq - https://github.com/zhshch2002/goreq

Gospider是一个轻量友好的的Go爬虫框架。

Gospider在管理网络请求方面使用了Goreq‌这样分割项目使功能划分更加明确,Gospider负责管理调度任务,Goreq负责处理网络请求。Gospider中的goreq.Requestgoreq.Responsegoreq.ClientGoreq提供。

# 🚀 Feature

  • 优雅的 API
  • 便于组织具有复杂层级和逻辑的代码
  • 友善的分布式支持
  • 一些细节 相对链接自动转换、字符编码自动解码、HTML/JSON 自动解析
  • 丰富的扩展支持 自动去重、失败重试、记录异常请求、控制延时、随机延时、并发、速率、Robots.txt 支持、随机 UA
  • 轻量 适于学习或快速开箱搭建

# ⚡ 网络请求

go get -u github.com/zhshch2002/goreq

Gospider依赖Goreq描述、完成网络请求,这是一个Goreq的简单演示,如需更多资料请查阅Goreq GitHub repo或者使用文档

fmt.Println(goreq.Get("https://httpbin.org/get").AddParam("A","a").Do().Txt())

结果是:

{
  "args": {
    "A": "a"
  }, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Host": "httpbin.org", 
    "User-Agent": "Go-http-client/2.0", 
    "X-Amzn-Trace-Id": "Root=1-6017ae9d-109027b5452abdd849d0161b"
  }, 
  "origin": "221.219.65.152", 
  "url": "https://httpbin.org/get?A=a"
}

此外:

  • resp.Resp() (*Response, error) 获取响应本身以及网络请求错误。
  • resp.Txt() (string, error) 自动处理完编码并解析为文本后的内容以及网络请求错误。
  • resp.HTML() (*goquery.Document, error)解析为HTML
  • resp.XML() (*xmlpath.Node, error)解析为XML
  • resp.BindXML(i interface{}) error将XML绑定到struct
  • resp.JSON() (gjson.Result, error)解析为JSON
  • resp.BindJSON(i interface{}) error将Json绑定到struct
  • resp.Error() error 网络请求错误。(正常情况下为nil

Goreq可以设置中间件、更换Http Client。请见Goreq 使用文档

# ⚡ 快速开始

go get -u github.com/zhshch2002/gospider

第一个例子:

package main

import (
	"github.com/zhshch2002/goreq"
	"github.com/zhshch2002/gospider"
)

func main() {
	s := gospider.NewSpider() // create spider

	s.OnResp(func(t *gospider.Task) {
		t.Println("this callback will process all response")
	})

	s.OnItem(func(t *gospider.Task, i interface{}) interface{} { // collect and save crawl result
		t.Println(i)
		return i
	})

	s.AddRootTask(
		goreq.Get("https://httpbin.org/get"),
		func(t *gospider.Task) { // this callback will only handle this request
			t.AddItem(t.Text) // submit result into OnItem pipeline
		},
	)

	s.Wait()
}

这是一个简单的爬虫,向https://httpbin.org/get发送请求并将结果作为Item存入SpiderGospider会异步处理OnItem结果,不阻塞爬虫进程。

# 任务(Task)

type Task struct {
   *goreq.Response
   s        *Spider
   Handlers []Handler
   Meta     map[string]interface{}
   abort    bool
}

任务本身就是爬虫能执行的最小单位。其中包含了请求(Req*goreq.Response中),此任务的处理函数Handlers,以及由上一个任务传递下来,并且会交给下一个任务的Meta数据。

# Abort

func main() {
   s := gospider.NewSpider() // create spider

   s.OnResp(func(t *gospider.Task) {
      t.Println("yep") // this will working as first handler
   })

   s.AddRootTask(
      goreq.Get("https://httpbin.org/get"),
      func(t *gospider.Task) {
         t.AddItem(t.Text) // this is second handler
         t.Abort() // abort handler pipeline here
      },
      func(t *gospider.Task) {
         t.Println("this wont be print")
      },
   )

   s.Wait()
}

# 新任务和Meta

有些情况下我们需要根据爬去的数据发起新的任务,比如抓去一个页面上的所有链接的内容。

s.AddRootTask正如其名,是用来创建根任务,意味着这个任务不是由任何正在执行的任务创建的。简而言之,爬去在第0层。其实Gospider中的任务都是从Task中创建来的,使用Task.AddTask函数。s.AddRootTask本身也创建了一个空Task,并调用了nilTask.AddTask

func main() {
   s := gospider.NewSpider() // create spider

   s.AddRootTask(
      goreq.Get("https://httpbin.org/"),
      func(t *gospider.Task) {
         h, _ := t.HTML()
         t.Meta["form"] = t.Req.URL

         h.Find("a[href]").Each(func(i int, sel *goquery.Selection) {
            t.AddTask(
               goreq.Get(sel.AttrOr("href", "")),
               func(t2 *gospider.Task) {
                  t2.Println(t2.Status, t2.Req.URL, "from", t2.Meta["form"])
               },
            )
         })
      },
   )

   s.Wait()
}

t.AddTask就是在蜘蛛运行中创建任务的方式。这样创建的任务带有执行的上下文,可以方便中间件和扩展计算爬去深度等重要数据。

例子中出现的Meta。在一个任务中设置的Meta数据,会被拷贝到新任务的Meta中,也就是继承上一级Meta的数据。如此可以在任务之间传递数据。

# 回调函数

在上面的例子中,实现爬虫功能主体的是s.OnItems.OnResps.AddRootTask里的回调函数。Gospider中对爬虫执行的不同阶段都可以设置回调函数,回调函数是Gospider处理数据的主要方式,其分为两类,对爬虫本体设置的生命周期回调函数任务自身处理函数

下面是Gospider所有的生命周期回调函数

  • **OnTask(fn func(o, t Task) Task) 创建新任务时调用,o为当前任务,t为新任务。
  • *OnResp(fn func(t Task))
  • *OnJSON(q string, fn func(t Task, j gjson.Result))
  • **OnHTML(selector string, fn func(t Task, sel goquery.Selection))
  • *OnItem(fn func(t Task, i interface{}) interface{})
  • *OnRecover(fn func(t Task, err error)) 当OnResp、OnItem或任务处理函数panic时调用。
  • *OnRespError(fn func(t Task, err error)) 当Response的Err属性不为空时调用。
  • *OnReqError(fn func(t Task, err error)) 当Request的Err属性不为空时调用。

flowchart

# Pipeline与Abort

上述回调中OnTask(fn func(o, t *Task) *Task)OnItem(fn func(t *Task, i interface{}) interface{})都带有返回值。

当注册多个OnItem时,上一个OnItem的返回值将会被传递给下一个OnItem,这将处理时修改item成为可能。同时,在此处返回nil将会中断此后的回调函数,中断后的回调函数都不会被执行。

package main

import (
   "github.com/zhshch2002/goreq"
   "github.com/zhshch2002/gospider"
)

func main() {
   s := gospider.NewSpider() // create spider

   s.OnItem(func(t *gospider.Task, i interface{}) interface{} {
      t.Println(i) // this is working
      return "yes!!"
   })

   s.OnItem(func(t *gospider.Task, i interface{}) interface{} {
      t.Println(i) // this is working
      return nil
   })

   s.OnItem(func(t *gospider.Task, i interface{}) interface{} {
      // this on item will never be called because last OnItem return nil
      t.Println(i)
      return i
   })

   s.AddRootTask(
      goreq.Get("https://httpbin.org/get"),
      func(t *gospider.Task) { // this callback will only handle this request
         t.AddItem(t.Text) // submit result into OnItem pipeline
      },
   )

   s.Wait()
}

OnTask中返回nil与直接Abort()的作用一致,nilAbort()都会中断整个任务,包括OnTask和之后的任何回调、处理函数。

# 扩展与中间件

s := gospider.NewSpider() // create spider

s.Use(gospider.WithDeduplicate())
s.Use(goreq.WithRandomUA())

上述程序中使用了两个中间件或扩展。一个是Goreq提供的内建中间件,一个是Gospider提供的内建扩展。

Use能接受的类型如下:

  • *func(goreq.Client, goreq.Handler) goreq.Handler 这是Goreq的中间件,使用时将会直接调用SpiderGoreqClient注册中间件。详细请参考Goreq的使用文档。
  • *func(s gospider.Spider)

由此可见,Gospider的扩展(不称之为中间件)本事是对Spider的一个配置函数。扩展的功能是通过注册回调函数来实现的。

func WithDeduplicate() Extension {
   return func(s *Spider) {
      CrawledHash := map[string]struct{}{}
      lock := sync.Mutex{}
      s.OnTask(func(o, t *Task) *Task {
         has := goreq.GetRequestHash(t.Req)
         lock.Lock()
         defer lock.Unlock()
         if _, ok := CrawledHash[has]; ok {
            return nil
         }
         CrawledHash[has] = struct{}{}
         return t
      })
   }
}

这是内建的请求去重的扩展。

# 内建扩展

# WithDeduplicate

func WithDeduplicate() Extension

根据goreq.GetRequestHash计算请求的哈希,在OnTaskdrop已有的请求。

# WithRobotsTxt

func WithRobotsTxt(ua string) Extension

自动处理robots.txt,参数为当前蜘蛛的ua

# WithDepthLimit

func WithDepthLimit(max int) Extension

限制爬取的最大深度。将在RequestContextWithValue来记录当前深度,keydepth。超过限制将在OnTask阶段被drop

# WithMaxReqLimit

func WithMaxReqLimit(max int64) Extension

使用OnTask限制最多请求数量。

# WithErrorLog

func WithErrorLog(f io.Writer) Extension

AddItem是一个error类型时,记录log

可以用来记录爬行时遇到的问题,如反爬虫程序和验证码等。

# WithCsvItemSaver

func WithCsvItemSaver(f io.Writer) Extension

将记录的Item写入csv文件中。

# 关于“气功波”式的代码

使用回调函数,以及Golang的闭包函数,无可奈何的会写出这样的代码。

func crawl() {
   s := gospider.NewSpider() // create spider

   s.AddRootTask(
      goreq.Get("https://example.com/"),
      func(t *gospider.Task) {
         h, _ := t.HTML()
         h.Find("div.a a[href]").Each(func(i int, sel *goquery.Selection) {
            t.AddTask(
               goreq.Get(sel.AttrOr("href", "")),
               func(t *gospider.Task) {
                  h, _ := t.HTML()
                  h.Find("div.b a[href]").Each(func(i int, sel *goquery.Selection) {
                     t.AddTask(
                        goreq.Get(sel.AttrOr("href", "")),
                        func(t *gospider.Task) {
                           h, _ := t.HTML()
                           h.Find("div.a a[href]").Each(func(i int, sel *goquery.Selection) {
                              t.AddTask(
                                 goreq.Get(sel.AttrOr("href", "")),
                                 func(t *gospider.Task) {

                                 },
                              )
                           })
                        },
                     )
                  })
               },
            )
         })
      })
}

就像是一个冲击波一样无限递进。

这个程序没有问题,正确的使用了框架。但是,不好看,也不优雅。

Gospider不限制程序具体如何编写,如果临时或者维护需求小且开发时间短,这样的程序很适合。

同时,Gospider还建议这样编写同样的爬虫。

type MySpider struct {}

func (m *MySpider) handler3(t *gospider.Task) {
   t.Println("hello", t.Req.URL)
}

func (m *MySpider) handler2(t *gospider.Task) {
   h, _ := t.HTML()
   h.Find("div.b a[href]").Each(func(i int, sel *goquery.Selection) {
      t.AddTask(
         goreq.Get(sel.AttrOr("href", "")),
         m.handler3,
      )
   },
   )
}

func (m *MySpider) handler1(t *gospider.Task) {
   h, _ := t.HTML()
   h.Find("div.b a[href]").Each(func(i int, sel *goquery.Selection) {
      t.AddTask(
         goreq.Get(sel.AttrOr("href", "")),
         m.handler2,
      )
   },
   )
}

func (m *MySpider) start() {
   s := gospider.NewSpider() // create spider

   s.AddRootTask(
      goreq.Get("https://example.com/"),
      m.handler1,
   )
}

# 关于分布式

Gospider本身没有分布式扩展或者支持,但也在积极探索相关功能和需求。

本身分布式要求对爬虫任务十分依赖而且因情况而异。通常可以分布式化的有任务派发网络请求(代理)数据处理结果收集等。

Golang本身提供了十分便利的环境使得这些部分可以在不破坏Gospider的情况下轻松实现。Gospider也会在将来把分布式化纳入计划中。

Last Updated: 3/16/2021, 2:53:33 AM