1.介绍 Colly
是Golang
世界中最知名的Web
爬虫框架,它提供简洁的 API,拥有强劲的性能、可以自动处理 cookie&session
、提供灵活的扩展机制,同时支持分布式抓取和多种存储后端(如内存、Redis
、MongoDB
等)。
2.安装 go get -u github.com/gocolly/colly/v2
3. 快速入门 3.1 语法模板 func collyUseTemplate () { collector := colly.NewCollector() collector.OnRequest(func (request *colly.Request) { fmt.Println("发起请求之前调用..." ) }) collector.OnError(func (response *colly.Response, err error ) { fmt.Println("请求期间发生错误,则调用:" ,err) }) collector.OnResponse(func (response *colly.Response) { fmt.Println("收到响应后调用:" ,response.Request.URL) }) collector.OnHTML("#position_shares table" , func (element *colly.HTMLElement) { }) err := collector.Visit("请求具体的地址" ) if err != nil { fmt.Println("具体错误:" ,err) } }
3.2 回调函数说明
OnRequest : 在发起请求之前调用。
OnError : 如果请求期间发生错误,则调用。
OnResponse :收到回复后调用。
OnHTML : 解析HTML内容 ,在OnResponse
之后调用。
OnXML : 如果收到的响应内容是XML 调用它。写爬虫基本用不到
OnScraped :在OnXML/OnHTML回调完成后调用。不过官网写的是Called after OnXML callbacks
,实际上对于OnHTML也有效,大家可以注意一下。
3.3 回调函数注册顺序
3.4 使用示例 需求: 抓取豆瓣小说榜单数据
1. 分析网页
2. 逻辑分析
第一步: 找到对应的ul
第二步: 遍历ul
,然后取出里面没一个li
的信息
3. 代码实现 package collyDemoimport ( "fmt" "github.com/gocolly/colly/v2" "strings" )func DouBanBook () error { collector := colly.NewCollector() collector.OnRequest(func (request *colly.Request) { fmt.Println("回调函数OnRequest: 在请求之前调用" ) }) collector.OnError(func (response *colly.Response, err error ) { fmt.Println("回调函数OnError: 请求错误" ,err) }) collector.OnResponse(func (response *colly.Response) { fmt.Println("回调函数OnResponse: 收到响应后调用" ) }) collector.OnHTML("ul[class='subject-list']" , func (element *colly.HTMLElement) { element.ForEach("li" , func (i int , el *colly.HTMLElement) { coverImg := el.ChildAttr("div[class='pic'] > a[class='nbg'] > img" ,"src" ) bookName := el.ChildText("div[class='info'] > h2" ) authorInfo := el.ChildText("div[class='info'] > div[class='pub']" ) split := strings.Split(authorInfo, "/" ) author := split[0 ] fmt.Printf("封面: %v 书名:%v 作者:%v\n" ,coverImg,trimSpace(bookName),author) }) }) return collector.Visit("https://book.douban.com/tag/小说" ) }func trimSpace (str string ) string { str = strings.ReplaceAll(str," " ,"" ) return strings.ReplaceAll(str,"\n" ,"" ) }
4. 运行结果 回调函数OnRequest: 在请求之前调用 回调函数OnResponse: 收到响应后调用 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https: 封面: https:
4. 配置采集器 4.1 配置预览
4.2 部分配置说明
AllowedDomains
: 设置收集器使用的域白名单,设置后不在白名单内链接,报错:Forbidden domain
。
AllowURLRevisit
: 设置收集器允许对同一 URL 进行多次下载。
Async
: 设置收集器为异步请求,需很Wait()
配合使用。
Debugger
: 开启Debug,开启后会打印请求日志。
MaxDepth
: 设置爬取页面的深度。
UserAgent
: 设置收集器使用的用户代理。
MaxBodySize
: 以字节为单位设置检索到的响应正文的限制。
IgnoreRobotsTxt
: 忽略目标机器中的robots.txt
声明。
4.3 创建采集器 collector := colly.NewCollector( colly.AllowedDomains("www.baidu.com" ,".baidu.com" ), colly.AllowURLRevisit(), colly.Async(true ), colly.Debugger(&debug.LogDebugger{}), colly.MaxDepth(2 ), colly.MaxBodySize(1024 * 1024 ), colly.UserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" ), colly.IgnoreRobotsTxt(), )
5. 常用解析函数 colly
爬取到页面之后,又该怎么解析Html
内容呢?实际上使用goquery 包解析这个页面,而colly.HTMLElement
其实就是对goquery.Selection
的简单封装:
type HTMLElement struct { Name string Text string attributes []html.Attribute Request *Request Response *Response DOM *goquery.Selection Index int }
5.1 Attr 1. 函数说明 func (h *HTMLElement) Attr(k string ) string
2. 使用示例
func TestUseAttr (t *testing.T) { collector := colly.NewCollector() collector.OnHTML("div[class='nav-logo'] > a" , func (element *colly.HTMLElement) { fmt.Printf("href:%v\n" ,element.Attr("href" )) }) _ = collector.Visit("https://book.douban.com/tag/小说" ) }
5.2 ChildAttr&ChildAttrs 1. 函数说明 func (h *HTMLElement) ChildAttr(goquerySelector, attrName string ) string func (h *HTMLElement) ChildAttrs(goquerySelector, attrName string ) []string
2. 使用示例
func TestChildAttrMethod (t *testing.T) { collector := colly.NewCollector() collector.OnError(func (response *colly.Response, err error ) { fmt.Println("OnError" ,err) }) collector.OnHTML("body" , func (element *colly.HTMLElement) { fmt.Printf("ChildAttr:%v\n" ,element.ChildAttr("div" ,"class" )) fmt.Printf("ChildAttrs:%v\n" ,element.ChildAttrs("div" ,"class" )) }) err := collector.Visit("https://liuqh.icu/a.html" ) if err != nil { fmt.Println("err" ,err) } }
5.3 ChildText & ChildTexts 1. 函数说明 func (h *HTMLElement) ChildText(goquerySelector string ) string func (h *HTMLElement) ChildTexts(goquerySelector string ) []string
2. 使用示例 a. 待解析的Html:
<html > <head > <title > 测试</title > <meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" /> </head > <body > <div class ="div1" > <div class ="sub1" > 内容1</div > </div > <div class ="div2" > 内容2</div > <div class ="div3" > 内容3</div > </body > </html >
b. 解析代码:
func TestChildTextMethod (t *testing.T) { collector := colly.NewCollector() collector.OnError(func (response *colly.Response, err error ) { fmt.Println("OnError" ,err) }) collector.OnHTML("body" , func (element *colly.HTMLElement) { fmt.Printf("ChildText:%v\n" ,element.ChildText("div" )) fmt.Printf("ChildTexts:%v\n" ,element.ChildTexts("div" )) }) err := collector.Visit("https://liuqh.icu/a.html" ) if err != nil { fmt.Println("err" ,err) } }
5.4 ForEach 1. 函数说明 func (h *HTMLElement) ForEach(goquerySelector string , callback func (int , *HTMLElement) )
2. 使用示例 a. 待解析Html:
<html > <head > <title > 测试</title > <meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" /> </head > <body > <ul class ="demo" > <li > <span class ="name" > 张三</span > <span class ="age" > 18</span > <span class ="home" > 北京</span > </li > <li > <span class ="name" > 李四</span > <span class ="age" > 22</span > <span class ="home" > 南京</span > </li > <li > <span class ="name" > 王五</span > <span class ="age" > 29</span > <span class ="home" > 天津</span > </li > </ul > </body > </html >
b.解析代码:
func TestForeach (t *testing.T) { collector := colly.NewCollector() collector.OnHTML("ul[class='demo']" , func (element *colly.HTMLElement) { element.ForEach("li" , func (_ int , el *colly.HTMLElement) { name := el.ChildText("span[class='name']" ) age := el.ChildText("span[class='age']" ) home := el.ChildText("span[class='home']" ) fmt.Printf("姓名: %s 年龄:%s 住址: %s \n" ,name,age,home) }) }) _ = collector.Visit("https://liuqh.icu/a.html" ) }
5.5 Unmarshal 1. 函数说明 func (h *HTMLElement) Unmarshal(v interface {}) error
2. 使用示例 a. 待解析Html
<html > <head > <title > 测试</title > <meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" /> </head > <body > <div class ="book" > <span class ="title" > <a href ="https://liuqh.icu" > 红楼梦</a > </span > <span class ="autor" > 曹雪芹 </span > <ul class ="category" > <li > 四大名著</li > <li > 文学著作</li > <li > 古典长篇章回小说</li > <li > 四大小说名著之首</li > </ul > <span class ="price" > 59.70元</span > </div > </body > </html >
b. 解析代码:
type Book struct { Name string `selector:"span.title"` Link string `selector:"span > a" attr:"href"` Author string `selector:"span.autor"` Reviews []string `selector:"ul.category > li"` Price string `selector:"span.price"` }func TestUnmarshal (t *testing.T) { var book Book collector := colly.NewCollector() collector.OnHTML("body" , func (element *colly.HTMLElement) { err := element.Unmarshal(&book) if err != nil { fmt.Println("解析失败:" ,err) } fmt.Printf("结果:%+v\n" ,book) }) _ = collector.Visit("https://liuqh.icu/a.html" ) }
6.常用选择器 6.1 html内容 <html > <head > <title > 测试</title > <meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" /> </head > <body > <div id ="title" > 标题ABC</div > <div class ="desc" > 这是一段描述</div > <span > 好好学习!</span > <div class ="parent" > <p class ="childA" > 老大</p > <p class ="childB" > 老二</p > </div > <span class ="context1" > 武松上山</span > <span class ="context2" > 打老虎</span > </body > </html >
6.2 使用示例 func TestSelector (t *testing.T) { collector := colly.NewCollector() collector.OnHTML("body" , func (element *colly.HTMLElement) { fmt.Printf("ID选择器使用: %v \n" ,element.ChildText("#title" )) fmt.Printf("class选择器使用1: %v \n" ,element.ChildText("div[class='desc']" )) fmt.Printf("class选择器使用2: %v \n" ,element.ChildText(".desc" )) fmt.Printf("相邻选择器: %v \n" ,element.ChildText("div[class='desc'] + span" )) fmt.Printf("父子选择器: %v \n" ,element.ChildText("div[class='parent'] > p" )) fmt.Printf("兄弟选择器: %v \n" ,element.ChildText("p[class='childA'] ~ p" )) fmt.Printf("同时选中多个1: %v \n" ,element.ChildText("span[class='context1'],span[class='context2']" )) fmt.Printf("同时选中多个2: %v \n" ,element.ChildText(".context1,.context2" )) }) _ = collector.Visit("https://liuqh.icu/a.html" ) }
7. 过滤器 7.1 第一个子元素(first-child & first-of-type
) 1. 过滤器说明
过滤器
说明
:first-child
筛选出父元素的第一个子元素,不分区子元素类型
:first-of-type
筛选出父元素的第一个指定类型子元素
2. html内容 <html > <head > <title > 测试</title > <meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" /> </head > <body > <div class ="parent" > <span > 第一个不是p标签</span > <p > 老大</p > <p > 老二</p > </div > <div class ="name" > <p > 张三</p > <p > 小米</p > </div > </body > </html >
3. 代码实现 func TestFilterFirstChild (t *testing.T) { collector := colly.NewCollector() collector.OnHTML("body" , func (element *colly.HTMLElement) { fmt.Printf("first-child: %v \n" ,element.ChildText("p:first-child" )) fmt.Printf("first-of-type: %v \n" ,element.ChildText("p:first-of-type" )) }) _ = collector.Visit("https://liuqh.icu/a.html" ) }
7.2 最后一个子元素(last-child & last-of-type
) 1. 过滤器说明
过滤器
说明
:last-child
筛选出父元素的最后一个子元素,不分区子元素类型
:last-of-type
筛选出父元素的最后一个指定类型子元素
使用方法和上面筛选第一个子元素
一样,不在啰嗦。
7.3 第x个子元素(nth-child & nth-of-type
) 1. 过滤器说明
过滤器
说明
nth-child(n)
筛选出的元素是其父元素的第n
个元素,n以1
开始。 所以:nth-child(1) = :first-child
nth-of-type(n)
和nth-child
一样,只不过它筛选的是同类型的第n个元素, 所以:nth-of-type(1) = :first-of-type
nth-last-child(n)
和nth-child(n)
一样,顺序是倒序
nth-last-of-type(n)
和nth-of-type(n)
一样,顺序是倒序
2. 使用示例 html内容和上面(7.1中)的html内容一样。
func TestFilterNth (t *testing.T) { collector := colly.NewCollector() collector.OnHTML("body" , func (element *colly.HTMLElement) { nthChild := element.ChildText("div[class='parent'] > :nth-child(1)" ) fmt.Printf("nth-child(1): %v \n" ,nthChild) nthOfType := element.ChildText("div[class='parent'] > p:nth-of-type(1)" ) fmt.Printf("nth-of-type(1): %v \n" ,nthOfType) nthLastChild := element.ChildText("div[class='parent'] > :nth-last-child(1)" ) fmt.Printf("nth-last-child(1): %v \n" ,nthLastChild) nthLastOfType := element.ChildText("div[class='parent'] > p:nth-last-of-type(1)" ) fmt.Printf("nth-last-of-type(1): %v \n" ,nthLastOfType) }) _ = collector.Visit("https://liuqh.icu/a.html" ) }
7.4 仅有一个子元素(only-child & only-of-type
) 1. 过滤器说明
过滤器
说明
:only-child
筛选其父元素下只有个子元素
:on-of-type
筛选其父元素下只有个指定类型的子元素
2. html内容 <html > <head > <title > 测试</title > <meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" /> </head > <body > <div class ="parent" > <span > 我是span标签</span > </div > <div class ="name" > <p > 我是p标签</p > </div > </body > </html >
3. 使用示例 func TestFilterOnly (t *testing.T) { collector := colly.NewCollector() collector.OnHTML("body" , func (element *colly.HTMLElement) { onlyChild := element.ChildTexts("div > :only-child" ) fmt.Printf("onlyChild: %v \n" ,onlyChild) nthOfType := element.ChildTexts("div > p:only-of-type" ) fmt.Printf("nth-of-type(1): %v \n" ,nthOfType) }) _ = collector.Visit("https://liuqh.icu/a.html" ) }
7.5 内容匹配(contains
) 1. html内容 <html > <head > <title > 测试</title > <meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" /> </head > <body > <a href ="https://www.baidu.com" > 百度</a > <a href ="https://cn.bing.com" > 必应</a > </body > </html >
2. 使用示例 func TestFilterContext (t *testing.T) { collector := colly.NewCollector() collector.OnHTML("body" , func (element *colly.HTMLElement) { attr1 := element.ChildAttr("a:contains(百度)" , "href" ) attr2 := element.ChildAttr("a:contains(必应)" , "href" ) fmt.Printf("百度: %v \n" ,attr1) fmt.Printf("必应: %v \n" ,attr2) }) _ = collector.Visit("https://liuqh.icu/a.html" ) }