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
->結束。
由剛才的例子,回撥函式收到的資料是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 - 仰望星空 如若資訊變動請以連結內版本為準。