共计 5068 个字符,预计需要花费 13 分钟才能阅读完成。
predator 是一款基于 fasthttp 开发的高性能爬虫框架。
以后版本尽管尚未实现全副性能,但已可应用。
应用
上面是一个示例,根本蕴含了以后已实现的所有性能,应用办法能够参考正文。
1 创立一个 Crawler
import "github.com/thep0y/predator"
func main() {
crawler := predator.NewCrawler(predator.WithUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0"),
predator.WithCookies(map[string]string{"JSESSIONID": cookie}),
predator.WithProxy(ip), // 或者应用代理池 predator.WithProxyPool([]string)
)
}
创立 Crawler
时有一些可选项用来性能加强。所有可选项参考 predator/options.go。
2 发送 Get 申请
crawler.Get("http://www.baidu.com")
对申请和响应的解决参考的是 colly,我感觉 colly 的解决形式十分难受。
// BeforeRequest 能够在发送申请前,对申请进行一些修补
crawler.BeforeRequest(func(r *predator.Request) {headers := map[string]string{
"Accept": "*/*",
"Accept-Language": "zh-CN",
"Accept-Encoding": "gzip, deflate",
"X-Requested-With": "XMLHttpRequest",
"Origin": "http://example.com",
}
r.SetHeaders(headers)
// 申请和响应之间的上下文传递,上下文见上面的上下文示例
r.Ctx.Put("id", 10)
r.Ctx.Put("name", "tom")
})
crawler.AfterResponse(func(r *predator.Response) {
// 从申请发送的上下文中取值
id := r.Ctx.GetAny("id").(int)
name := r.Ctx.Get("name")
// 对于 json 响应,倡议应用 gjson 进行解决
body := gjson.ParseBytes(r.Body)
amount := body.Get("amount").Int()
types := body.Get("types").Array()})
// 申请语句要在 BeforeRequest 和 AfterResponse 前面调用
crawler.Get("http://www.baidu.com")
3 发送 Post 申请
与 Get 申请有一点不同,通常每个 Post 的申请的参数是不同的,而这些参数都在申请体中,在 BeforeRequest
中解决申请体尽管能够,但绝非最佳抉择,所以在结构 Post 申请时,能够间接传入上下文,用以解决与响应的信息传递。
// BeforeRequest 能够在发送申请前,对申请进行一些修补
crawler.BeforeRequest(func(r *predator.Request) {headers := map[string]string{
"Accept": "*/*",
"Accept-Language": "zh-CN",
"Accept-Encoding": "gzip, deflate",
"X-Requested-With": "XMLHttpRequest",
"Origin": "http://example.com",
}
r.SetHeaders(headers)
})
crawler.AfterResponse(func(r *predator.Response) {
// 从申请发送的上下文中取值
id := r.Ctx.GetAny("id").(int)
name := r.Ctx.Get("name")
// 对于 json 响应,倡议应用 gjson 进行解决
body := gjson.ParseBytes(r.Body)
amount := body.Get("amount").Int()
types := body.Get("types").Array()})
body := map[string]string{"foo": "bar"}
// 在 Post 申请中,应该将要害参数用这种形式放进上下文
ctx, _ := context.AcquireCtx()
ctx.Put("id", 10)
ctx.Put("name", "tom")
crawler.Post("http://www.baidu.com", body, ctx)
如果不须要传入上下文,能够间接用 nil
代替:
crawler.Post("http://www.baidu.com", body, nil)
4 发送 multipart/form-data 申请
multipart/form-data
办法须要应用专门的 PostMultipart
办法,只是以后申请体只反对map[string]string
,没有别的起因,因为我只用到这种类型,如果当前有别的需要,再改成map[string]interface{}
。
参考示例:https://github.com/thep0y/pre…
5 上下文
上下文是一个接口,我实现了两种上下文:
- ReadOp:基于
sync.Map
实现,实用于读取上下文较多的场景 - WriteOp:用
map
实现,实用于读写频率相差不大或写多于读的场景,这是默认采纳的上下文
爬虫中如果遇到了读远多于写时就应该换 ReadOp
了,如下代码所示:
ctx, err := AcquireCtx(context.ReadOp)
6 解决 HTML
爬虫的后果大体可分为两种,一是 HTML 响应,另一种是 JSON 格局的响应。
与 JSON 相比,HTML 须要更多的代码解决。
本框架对 HTML 解决进行了一些函数封装,能不便地通过 css selector 进行元素的查找,能够提取元素中的属性和文本等。
crawl := NewCrawler()
crawl.ParseHTML("body", func(he *html.HTMLElement) {
// 元素外部 HTML
h, err := he.InnerHTML()
// 元素整体 HTML
h, err := he.OuterHTML()
// 元素内的文本(包含子元素的文本)he.Text()
// 元素的属性
he.Attr("class")
// 第一个匹配的子元素
he.FirstChild("p")
// 最初一个匹配的子元素
he.LastChild("p")
// 第 2 个匹配的子元素
he.Child("p", 2)
// 第一个匹配的子元素的属性
he.ChildAttr("p", "class")
// 所有匹配到的子元素的属性切片
he.ChildrenAttr("p", "class")
}
7 异步 / 多协程申请
c := NewCrawler(
// 应用此 option 时主动应用指定数量的协程池发出请求,不应用此 option 则默认应用同步形式申请
// 设置的数量不宜过少,也不宜过多,请自行测试设置不同数量时的效率
WithConcurrency(30),
)
c.AfterResponse(func(r *predator.Response) {// handle response})
for i := 0; i < 10; i++ {c.Post(ts.URL+"/post", map[string]string{"id": fmt.Sprint(i + 1),
}, nil)
}
c.Wait()
8 应用缓存
默认状况下,缓存是不启用的,所有的申请都间接放行。
曾经实现的缓存:
- MySQL
- PostgreSQL
- Redis
- SQLite3
缓存接口中有一个办法 Compressed(yes bool)
用来压缩响应的,毕竟有时,响应长度十分长,间接保留到数据库中会影响插入和查问时的性能。
这四个接口的应用办法示例:
// MySQL
c := NewCrawler(
WithCache(&cache.MySQLCache{
Host: "127.0.0.1",
Port: "3306",
Database: "predator",
Username: "root",
Password: "123456",
}, false), // false 为敞开压缩,true 为开启压缩,下同
)
// PostgreSQL
c := NewCrawler(
WithCache(&cache.PostgreSQLCache{
Host: "127.0.0.1",
Port: "54322",
Database: "predator",
Username: "postgres",
Password: "123456",
}, false),
)
// Redis
c := NewCrawler(
WithCache(&cache.RedisCache{Addr: "localhost:6379",}, true),
)
// SQLite3
c := NewCrawler(
WithCache(&cache.SQLiteCache{URI: uri, // uri 为数据库寄存的地位,尽量加上后缀名 .sqlite}, true),
)
// 也能够应用默认值。WithCache 的第一个为 nil 时,// 默认应用 SQLite 作为缓存,且会将缓存保留在以后
// 目录下的 predator-cache.sqlite 中
c := NewCrawler(WithCache(nil, true))
9 代理
反对 HTTP 代理和 Socks5 代理。
应用代理时须要加上协定,如:
WithProxyPool([]string{"http://ip:port", "socks5://ip:port"})
10 对于 JSON
原本想着封装一个 JSON 包用来疾速解决 JSON 响应,然而想了一两天也没想出个好方法来,因为我能想到的,gjson 都曾经解决了。
对于 JSON 响应,能用 gjson
解决就不要老想着反序列化了。对于爬虫而言,反序列化是不明智的抉择。
当然,如果你的确有反序列化的需要,也不要用规范库,应用封装的 JSON 包中的序列化和反序列化办法比规范库性能高。
import "github.com/thep0y/predator/json"
json.Marshal()
json.Unmarshal()
json.UnmarshalFromString()
凑合 JSON 响应,以后足够用了。
指标
- [x] 实现对失败响应的从新申请,直到重试了传入的重试次数时才算最终申请失败
-
[x] 辨认因代理生效而造成的申请失败。当应用代理池时,代理池中剔除此代理;代理池为空时,终止整个爬虫程序
- 思考到应用代理必然是因为不想将本地 ip 裸露给指标网站或服务器,所以在应用代理后,当所有代理都生效时,不再持续发出请求
- [x] HTML 页面解析。不便定位查找元素
-
[x] json 扩大,用来解决、筛选 json 响应的数据,原生 json 库不适宜用在爬虫上
- 临时没想到如何封装便捷好用的 json,以后 json 包中只能算是应用示例
- [x] 协程池,实现在多协程时对每个 goroutine 的复用,防止反复创立
-
[x] 定义缓存接口,并实现一种或多种缓存。因为长期缓存在爬虫中并不实用,所以 predator 采纳长久化缓存。
- 默认应用 sqlite3 进行缓存,能够应用已实现的其余缓存数据库,也能够本人实现缓存接口
- 可用缓存存储有 SQLite3、MySQL、PostgreSQL、Redis
- 因为采纳长久化缓存,所以不实现以内存作为缓存,如果须要请自行依据缓存接口实现
-
[x] 数据库治理接口,用来保留爬虫数据,并实现一种或多种数据库的治理
- SQL 数据库接口已实现了,NoSQL 接口与 SQL 差异较大,就不实现了,如果有应用 NoSQL 的需要,请本人实现
- 数据库接口没有封装在 Crawler 办法中,依据须要应用,个别场景下够用,简单场景中依然须要本人重写数据库治理
- [] 增加日志
- [] 为
Request
和Response
的申请体Body
增加池治理,缩小 GC 次数 - [] 减少对 robots.txt 的判断,默认恪守 robots.txt 规定,但能够抉择疏忽
我的项目地址
thep0y/predator: High-performance crawler framework based on fasthttp (github.com)