Go 常用包: 知名爬蟲框架 Colly

  1. 介紹

CollyGolang世界中最知名的Web爬蟲框架, 它提供簡潔的 API,擁有強勁的性能、可以自動處理 cookie&session、提供靈活的擴展機制, 同時支持分佈式抓取。

  1. 安裝

// 下載最新版本
go get -u github.com/gocolly/colly/v2
  1. 快速入門

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 回調函數說明

3.3 回調函數註冊順序

image-20210819234814412

3.4 使用示例

需求: 抓取豆瓣小說榜單數據

1. 分析網頁

2. 邏輯分析

3. 代碼實現

package collyDemo

import (
 "fmt"
 "github.com/gocolly/colly/v2"
 "strings"
)
// 豆瓣書榜單
func DouBanBook() error {
 // 創建 Collector 對象
 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: 收到響應後調用")
 })
 //OnResponse如果收到的內容是HTML ,則在之後調用
 collector.OnHTML("ul[class='subject-list']", func(element *colly.HTMLElement) {
  // 遍歷li
  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://img9.doubanio.com/view/subject/s/public/s27279654.jpg 書名:活着 作者:餘華 
封面: https://img1.doubanio.com/view/subject/s/public/s33880929.jpg 書名:字母表謎案 作者:大山誠一郎 
封面: https://img1.doubanio.com/view/subject/s/public/s24514468.jpg 書名:白夜行 作者:[] 東野圭吾 
封面: https://img2.doubanio.com/view/subject/s/public/s29651121.jpg 書名:房思琪的初戀樂園 作者:林奕含 
封面: https://img1.doubanio.com/view/subject/s/public/s33834057.jpg 書名:文城 作者:餘華 
封面: https://img3.doubanio.com/view/subject/s/public/s27237850.jpg 書名:百年孤獨 作者:[哥倫比亞] 加西亞·馬爾克斯 
封面: https://img9.doubanio.com/view/subject/s/public/s33944156.jpg 書名:蛤蟆先生去看心理醫生 作者:【英】羅伯特·戴博德 
封面: https://img1.doubanio.com/view/subject/s/public/s1070959.jpg 書名:紅樓夢 作者:[] 曹雪芹 著、高鶚 續 
封面: https://img9.doubanio.com/view/subject/s/public/s33821754.jpg 書名:克拉拉與太陽 作者:[] 石黑一雄 
封面: https://img2.doubanio.com/view/subject/s/public/s1103152.jpg 書名:小王子 作者:[] 聖埃克蘇佩裏 
封面: https://img2.doubanio.com/view/subject/s/public/s27264181.jpg 書名:解憂雜貨店 作者:[] 東野圭吾 
封面: https://img1.doubanio.com/view/subject/s/public/s2768378.jpg 書名:三體:“地球往事”三部曲之一 作者:劉慈欣 
封面: https://img3.doubanio.com/view/subject/s/public/s1727290.jpg 書名:追風箏的人 作者:[] 卡勒德·胡賽尼 
封面: https://img2.doubanio.com/view/subject/s/public/s11284102.jpg 書名:霍亂時期的愛情 作者:[哥倫比亞] 加西亞·馬爾克斯 
封面: https://img3.doubanio.com/view/subject/s/public/s24575140.jpg 書名:許三觀賣血記 作者:餘華 
封面: https://img1.doubanio.com/view/subject/s/public/s4371408.jpg 書名:1984 作者:[] 喬治·奧威爾 
封面: https://img9.doubanio.com/view/subject/s/public/s4468484.jpg 書名:局外人 作者:[] 阿爾貝·加繆 
封面: https://img9.doubanio.com/view/subject/s/public/s28357056.jpg 書名:三體全集:地球往事三部曲 作者:劉慈欣 
封面: https://img9.doubanio.com/view/subject/s/public/s29799055.jpg 書名:雲邊有個小賣部 作者:張嘉佳 
封面: https://img3.doubanio.com/view/subject/s/public/s33718940.jpg 書名:夜晚的潛水艇 作者:陳春成
  1. 配置採集器

4.1 配置預覽

4.2 部分配置說明

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`聲明
 )
  1. 常用解析函數

colly爬取到頁面之後,又該怎麼解析Html內容呢?實際上使用 goquery 包解析這個頁面, 而colly.HTMLElement其實就是對goquery.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
*/
  1. 常用選擇器

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
*/
  1. 過濾器

7.1 第一個子元素 (first-child & first-of-type)

1. 過濾器說明

uWGV4a

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
*/

7.2 最後一個子元素 (last-child & last-of-type)

1. 過濾器說明

FMA49y

使用方法和上面篩選第一個子元素一樣,不在囉嗦。

7.3 第 x 個子元素 (nth-child & nth-of-type)

1. 過濾器說明

uJFhN7

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
*/

7.4 僅有一個子元素 (only-child & only-of-type)

1. 過濾器說明

ODXvAo

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
*/

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")
}
/**輸出
=== RUN   TestFilterContext
百度: https://www.baidu.com 
必應: https://cn.bing.com 
--- PASS: TestFilterContext (0.31s)
PASS
*/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/23Waiw9emWkfSQxnf96hHQ