Go 常用包: 知名爬蟲框架 Colly
- 介紹
Colly
是Golang
世界中最知名的Web
爬蟲框架, 它提供簡潔的 API,擁有強勁的性能、可以自動處理 cookie&session
、提供靈活的擴展機制, 同時支持分佈式抓取。
- 安裝
// 下載最新版本
go get -u github.com/gocolly/colly/v2
- 快速入門
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 回調函數說明
-
OnRequest: 在發起請求之前調用。
-
OnError: 如果請求期間發生錯誤, 則調用。
-
OnResponse:收到回覆後調用。
-
OnHTML: 解析 HTML 內容 , 在
OnResponse
之後調用。 -
OnXML: 如果收到的響應內容是 XML 調用它。寫爬蟲基本用不到
-
OnScraped:在 OnXML/OnHTML 回調完成後調用。不過官網寫的是
Called after OnXML callbacks
,實際上對於 OnHTML 也有效,大家可以注意一下。
3.3 回調函數註冊順序
image-20210819234814412
3.4 使用示例
需求: 抓取豆瓣小說榜單數據
1. 分析網頁
2. 邏輯分析
-
第一步: 找到對應的
ul
-
第二步: 遍歷
ul
, 然後取出裏面沒一個li
的信息
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 書名:夜晚的潛水艇 作者:陳春成
- 配置採集器
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(),//允許對同一 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`聲明
)
- 常用解析函數
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
*/
- 常用選擇器
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.1 第一個子元素 (first-child & first-of-type
)
1. 過濾器說明
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. 過濾器說明
使用方法和上面篩選第一個子元素
一樣,不在囉嗦。
7.3 第 x 個子元素 (nth-child & nth-of-type
)
1. 過濾器說明
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. 過濾器說明
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