1.介绍

CollyGolangWebcookie&session

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)
 })
 //OnResponse如果收到的内容是HTML ,则在之后调用
 collector.OnHTML("#position_shares table", func(element *colly.HTMLElement) {
  // todo 解析html内容
 })
 // url:请求具体的地址
 err := collector.Visit("请求具体的地址")
 if err != nil {
  fmt.Println("具体错误:",err)
 }
}

3.2 回调函数说明

OnResponseCalled after OnXML callbacks

3.3 回调函数注册顺序

image-20210819234814412

3.4 使用示例

需求: 抓取豆瓣小说榜单数据

1. 分析网页

2. 逻辑分析

ululli

3. 代码实现


4. 运行结果


4. 配置采集器

4.1 配置预览

4.2 部分配置说明

AllowedDomainsForbidden domainAllowURLRevisitAsyncWait()DebuggerMaxDepthUserAgentMaxBodySizeIgnoreRobotsTxtrobots.txt

4.3 创建采集器

 collector := colly.NewCollector(
  colly.AllowedDomains("www.baidu.com",".baidu.com"),//白名单域名
  colly.AllowURLRevisit(),//允许对同一 URL 进行多次下载
  colly.Async(true),//设置为异步请求
  colly.Debugger(&debug.LogDebugger{}),// 开启debug
  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(),//忽略目标机器中的`robots.txt`声明
 )

5. 常用解析函数

collyHtmlcolly.HTMLElementgoquery.Selection
// colly.HTMLElement结构体
type HTMLElement struct {
 Name       string
 Text       string
 attributes []html.Attribute
 Request *Request
 Response *Response
 // 对goquery封装
 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()
  // 定位到div[class='nav-logo'] > a标签元素
 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/小说")
}
/**输出
=== RUN   TestUseAttr
href:https://book.douban.com
--- PASS: TestUseAttr (0.66s)
PASS
*/

5.2 ChildAttr&ChildAttrs

1. 函数说明

// 返回`goquerySelector`选择的第一个子元素的`attrName`属性;
func (h *HTMLElement) ChildAttr(goquerySelector, attrName string) string
// 返回`goquerySelector`选择的所有子元素的`attrName`属性,以`[]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)
 })
 // 解析Html
 collector.OnHTML("body", func(element *colly.HTMLElement) {
  // 获取第一个子元素(div)的class属性
  fmt.Printf("ChildAttr:%v\n",element.ChildAttr("div","class"))
  // 获取所有子元素(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)
 }
}
/**输出
=== RUN   TestChildAttrMethod
ChildAttr:div1
ChildAttrs:[div1 sub1 div2 div3]
--- PASS: TestChildAttrMethod (0.29s)
PASS
*/

5.3 ChildText & ChildTexts

1. 函数说明

// 拼接goquerySelector选择的子元素的文本内容并返回
func (h *HTMLElement) ChildText(goquerySelector string) string 
// 返回goquerySelector选择的子元素的文本内容组成的切片,以[]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. 解析代码:

// 测试使用ChildText和ChildTexts
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) {
  // 获取第一个子元素(div)的class属性
  fmt.Printf("ChildText:%v\n",element.ChildText("div"))
  // 获取所有子元素(div)的class属性
  fmt.Printf("ChildTexts:%v\n",element.ChildTexts("div"))
 })
 err := collector.Visit("https://liuqh.icu/a.html")
 if err != nil {
  fmt.Println("err",err)
 }
}
/**输出
=== RUN   TestChildTextMethod
ChildText:内容1
 内容1内容2内容3
ChildTexts:[内容1 内容1 内容2 内容3]
--- PASS: TestChildTextMethod (0.28s)
PASS
*/

5.4 ForEach

1. 函数说明

//对每个`goquerySelector`选择的子元素执行回调`callback`
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")
}
/**输出
=== RUN   TestForeach
姓名: 张三  年龄:18 住址: 北京 
姓名: 李四  年龄:22 住址: 南京 
姓名: 王五  年龄:29 住址: 天津 
--- PASS: TestForeach (0.36s)
PASS
*/

5.5 Unmarshal

1. 函数说明

// 通过给结构体字段指定 goquerySelector 格式的 tag,可以将HTMLElement对象Unmarshal 到一个结构体实例中
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")
}

/***输出
=== RUN   TestUnmarshal
结果:{Name:红楼梦 Link:https://liuqh.icu Author:曹雪芹 Reviews:[四大名著 文学著作 古典长篇章回小说 四大小说名著之首] Price:59.70元}
--- PASS: TestUnmarshal (0.27s)
PASS
*/

6.常用选择器

6.1 html内容

<html>
<head>
  <title>测试</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
  <!-- 演示ID选择器 -->
  <div id="title">标题ABC</div>
  <!-- 演示class选择器 -->
  <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) {
  // ID属性选择器,使用#
  fmt.Printf("ID选择器使用: %v \n",element.ChildText("#title"))
  // Class属性选择器,使用
  fmt.Printf("class选择器使用1: %v \n",element.ChildText("div[class='desc']"))
  fmt.Printf("class选择器使用2: %v \n",element.ChildText(".desc"))
  // 相邻选择器 prev +  next: 提取 <span>好好学习!</span>
  fmt.Printf("相邻选择器: %v \n",element.ChildText("div[class='desc'] + span"))
  // 父子选择器: parent > child,提取:<div class="parent">下所有子元素
  fmt.Printf("父子选择器: %v \n",element.ChildText("div[class='parent'] > p"))
  // 兄弟选择器 prev ~ next , 提取:<p class="childB">老二</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")
}

/**输出
=== RUN   TestSelector
ID选择器使用: 标题ABC 
class选择器使用1: 这是一段描述 
class选择器使用2: 这是一段描述 
相邻选择器: 好好学习! 
父子选择器: 老大老二 
兄弟选择器: 老二 
同时选中多个1: 武松上山打老虎 
同时选中多个2: 武松上山打老虎 
--- PASS: TestSelector (0.31s)
PASS
*/

7. 过滤器

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">
    <!--因为这里的p不是第一个子元素,不会被first-child筛选到-->
   <span>第一个不是p标签</span>
    <p>老大</p>
   <p>老二</p>
  </div>
   <div class="name">
    <p>张三</p>
   <p>小米</p>
  </div>
</body>
</html>

3. 代码实现

// 常用过滤器使用first-child & first-of-type
func TestFilterFirstChild(t *testing.T) {
 collector := colly.NewCollector()
 collector.OnHTML("body", func(element *colly.HTMLElement) {
  // 只会筛选父元素下第一个子元素是<p>..</p>
  fmt.Printf("first-child: %v \n",element.ChildText("p:first-child"))
  // 会筛选父元素下第一个子元素类型是<p>..</p>
  fmt.Printf("first-of-type: %v \n",element.ChildText("p:first-of-type"))
 })
 _ = collector.Visit("https://liuqh.icu/a.html")
}
/**输出
=== RUN   TestFilterFirstChild
first-child: 张三 
first-of-type: 老大张三 
--- PASS: TestFilterFirstChild (0.51s)
PASS
*/

last-child & last-of-type

1. 过滤器说明

:last-child:last-of-type
筛选第一个子元素
nth-child & nth-of-type

1. 过滤器说明

nth-child(n)n1:nth-child(1) = :first-childnth-of-type(n)nth-child:nth-of-type(1) = :first-of-typenth-last-child(n)nth-child(n)nth-last-of-type(n)nth-of-type(n)

2. 使用示例

html内容和上面(7.1中)的html内容一样。

// 过滤器第x个元素
func TestFilterNth(t *testing.T) {
 collector := colly.NewCollector()
 collector.OnHTML("body", func(element *colly.HTMLElement) {
  //<div class="parent">下的第一个子元素
  nthChild := element.ChildText("div[class='parent'] > :nth-child(1)")
  fmt.Printf("nth-child(1): %v \n",nthChild)

  //<div class="parent">下的第一个p子元素
  nthOfType := element.ChildText("div[class='parent'] > p:nth-of-type(1)")
  fmt.Printf("nth-of-type(1): %v \n",nthOfType)

  // div class="parent">下的最后一个子元素
  nthLastChild := element.ChildText("div[class='parent'] > :nth-last-child(1)")
  fmt.Printf("nth-last-child(1): %v \n",nthLastChild)

  //<div class="parent">下的最后一个p子元素
  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")
}
/**输出
=== RUN   TestFilterNth
nth-child(1): 第一个不是p标签 
nth-of-type(1): 老大 
nth-last-child(1): 老二 
nth-last-of-type(1): 老二 
--- PASS: TestFilterNth (0.29s)
PASS
*/

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)
  // 匹配其子元素:有且只有一个 p 标签的
  nthOfType := element.ChildTexts("div > p:only-of-type")
  fmt.Printf("nth-of-type(1): %v \n",nthOfType)

 })
 _ = collector.Visit("https://liuqh.icu/a.html")
}
/**输出
=== RUN   TestFilterOnly
onlyChild: [我是span标签 我是p标签] 
nth-of-type(1): [我是p标签] 
--- PASS: TestFilterOnly (0.29s)
PASS
*/

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")
}
/**输出
=== RUN   TestFilterContext
百度: https://www.baidu.com 
必应: https://cn.bing.com 
--- PASS: TestFilterContext (0.31s)
PASS
*/