Go語言爬蟲系列教程5:HTML解析技術以及第三方庫選擇
在上一章中,我們使用正則表達式提取網頁內容,但這種方法有局限性。對于復雜的HTML結構,我們需要使用專門的HTML解析庫。在這一章中,我們將介紹HTML解析技術以及如何選擇合適的第三方庫。
一、HTML DOM樹結構介紹
1.1 什么是DOM
在學習HTML解析之前,我們需要了解HTML的DOM樹結構。DOM(Document Object Model)是HTML文檔的樹形結構表示,它將HTML文檔中的每個元素、屬性和文本都表示為一個節點。
1.2 DOM樹的基本組成
DOM樹由多種類型的節點組成:
- 元素節點:對應HTML標簽,如
<div>
、<p>
等 - 文本節點:包含文本內容
- 屬性節點:元素的屬性,如
class="container"
- 注釋節點:HTML注釋
<!-- 注釋 -->
1.3 節點關系
DOM樹中的節點具有以下關系:
- 父子關系:包含其他元素的節點是父節點,被包含的是子節點
- 兄弟關系:共享同一父節點的節點互為兄弟節點
- 祖先和后代關系:間接的父子關系
1.4 DOM樹示例
以下是一個簡單HTML文檔及其DOM樹結構:
<!DOCTYPE html>
<html>
<head><title>示例頁面</title>
</head>
<body><div class="container"><h1>標題</h1><p>這是<a href="https://example.com">一個</a>段落。</p></div>
</body>
</html>
其樹狀結構可以表示為:
html
├── head
│ └── title
│ └── "示例頁面"
└── body└── div.container├── h1│ └── "標題"└── p├── "這是"├── a[href="https://example.com"]│ └── "一個"└── "段落。"
1.5 HTML解析的重要性
理解DOM樹結構對于HTML解析至關重要,因為它是我們進行網頁數據提取的基礎。
- 從網頁中提取結構化數據
- 查找特定的元素和屬性
- 分析網頁結構
- 數據清洗和處理
二、CSS選擇器詳解
CSS選擇器是一種用于選擇HTML元素的語法。它不僅在CSS樣式中使用,也是HTML解析庫中定位元素的重要工具。
2.1 基礎選擇器
元素選擇器
選擇指定標簽的所有元素:
p /* 選擇所有<p>元素 */
div /* 選擇所有<div>元素 */
h1 /* 選擇所有<h1>元素 */
類選擇器
選擇具有指定class的元素,以.
開頭:
.quote /* 選擇所有class="quote"的元素 */
.container /* 選擇所有class="container"的元素 */
ID選擇器
選擇具有指定id的元素,以#
開頭:
#header /* 選擇id="header"的元素 */
#main /* 選擇id="main"的元素 */
2.2 組合選擇器
后代選擇器(空格)
選擇某元素內部的所有指定元素:
.quote .text /* 選擇class="quote"元素內的class="text"元素 */
div p /* 選擇div內的所有p元素 */
子元素選擇器(>)
選擇某元素的直接子元素:
.quote > .text /* 選擇class="quote"元素的直接子元素中class="text"的元素 */
ul > li /* 選擇ul的直接子元素li */
相鄰兄弟選擇器(+)
選擇緊接在指定元素后的兄弟元素:
h1 + p /* 選擇緊跟在h1后的p元素 */
.title + .author /* 選擇緊跟在class="title"后的class="author"元素 */
通用兄弟選擇器(~)
選擇指定元素后的所有兄弟元素:
h1 ~ p /* 選擇h1后的所有兄弟p元素 */
.title ~ .info /* 選擇class="title"后的所有class="info"兄弟元素 */
2.3 屬性選擇器
選擇器 | 說明 | 示例 |
---|---|---|
[attr] | 選擇具有指定屬性的元素 | a[href] |
[attr=value] | 選擇屬性值完全匹配的元素 | input[type="text"] |
[attr!=value] | 選擇屬性值不等于指定值的元素 | input[type!="hidden"] |
[attr^=value] | 選擇屬性值以指定值開頭的元素 | a[href^="https"] |
[attr$=value] | 選擇屬性值以指定值結尾的元素 | img[src$=".jpg"] |
[attr*=value] | 選擇屬性值包含指定值的元素 | class[*="nav"] |
[attr~=value] | 選擇屬性值包含指定單詞的元素 | class[~="active"] |
`[attr | =value]` | 選擇屬性值等于指定值或以指定值開頭后跟連字符的元素 |
2.4 偽類選擇器
結構偽類
:first-child /* 選擇作為第一個子元素的元素 */
:last-child /* 選擇作為最后一個子元素的元素 */
:nth-child(n) /* 選擇作為第n個子元素的元素 */
:nth-last-child(n) /* 選擇作為倒數第n個子元素的元素 */
:only-child /* 選擇作為唯一子元素的元素 */:first-of-type /* 選擇同類型中的第一個元素 */
:last-of-type /* 選擇同類型中的最后一個元素 */
:nth-of-type(n) /* 選擇同類型中的第n個元素 */
:nth-last-of-type(n) /* 選擇同類型中的倒數第n個元素 */
:only-of-type /* 選擇同類型中的唯一元素 */
內容偽類
:empty /* 選擇沒有子元素和文本內容的元素 */
:contains(text) /* 選擇包含指定文本的元素(goquery特有) */
:has(selector) /* 選擇包含匹配選擇器的子元素的元素 */
2.5 選擇器優先級
當多個選擇器作用于同一元素時,優先級規則如下(從高到低):
- 內聯樣式:
style="..."
- ID選擇器:
#id
- 類選擇器、屬性選擇器、偽類:
.class
、[attr]
、:hover
- 元素選擇器、偽元素:
div
、::before
三、Go中的HTML解析庫:goquery
3.1 goquery簡介
goquery是Go語言的一個強大HTML解析庫,靈感來自jQuery。它基于Go標準庫中的net/html包,并提供了類似jQuery的鏈式API,使HTML文檔的遍歷和操作變得簡單。
goquery的主要特點包括:
- 簡單易用的API
- 高效的解析性能
- 支持CSS選擇器
3.2 安裝goquery
使用以下命令安裝goquery:
go get github.com/PuerkitoBio/goquery
3.3 創建文檔
在使用goquery之前,首先需要創建一個文檔對象。這相當于將HTML轉換成可以用代碼操作的結構。
3.3.1 從字符串創建文檔
package mainimport ("fmt""github.com/PuerkitoBio/goquery""strings"
)func main() {html := `<html><body><div class="content"><h1>標題</h1><p>這是段落</p></div></body></html>`// 將HTML字符串轉換為可讀取的對象reader := strings.NewReader(html)doc, err := goquery.NewDocumentFromReader(reader)if err != nil {fmt.Println("加載HTML出錯:", err)return}// 現在我們有了一個doc對象,可以用它來查找元素title := doc.Find("h1").Text()content := doc.Find("p").Text()fmt.Println("標題:", title)fmt.Println("內容:", content)
}
得到的結果:
標題: 標題
內容: 這是段落
NewDocumentFromReader()
從字符串創建一個新的文檔,?返回了一個*Document和error。Document代表一個將要被操作的HTML文檔。
Find()
主要是用來查找元素, Find("h1")
即代表獲取html中h1
標簽的元素,包括它的子元素。如果有多個h1
標簽,默認獲取的是最后一個。
Text()
獲取元素的純文本內容
3.3.2 從網絡加載文檔
如果你還記得上一章的內容,我們使用 https://quotes.toscrape.com/ 作為演示,使用正則表達式獲取了引言和作者,現在我們使用goquery代替
package mainimport ("fmt""github.com/PuerkitoBio/goquery""net/http"
)func main() {// 發送HTTP請求獲取網頁resp, err := http.Get("https://quotes.toscrape.com/")if err != nil {fmt.Println("請求網頁失敗:", err)return}defer resp.Body.Close() // 記得關閉連接// 檢查HTTP狀態碼if resp.StatusCode != 200 {fmt.Printf("狀態碼不對: %d %s\n", resp.StatusCode, resp.Status)return}// 從響應創建文檔doc, err := goquery.NewDocumentFromReader(resp.Body)if err != nil {fmt.Println("解析HTML失敗:", err)return}// 使用文檔quote := doc.Find(".text").Text()author := doc.Find(".author").Text()fmt.Println("引言:", quote)fmt.Println("作者:", author)
}
.text
和.author
都是類選擇器,代表class="quote"
和class="author"
類名前加.
表示類
3.4 goquery常用方法介紹
3.4.1 Find方法 - 查找所有匹配元素
Find
方法是最基礎也是最常用的方法,它可以查找符合CSS選擇器的所有元素。
// 查找所有段落
paragraphs := doc.Find("p")// 獲取找到的元素數量
count := paragraphs.Length()
fmt.Printf("找到了%d個段落\n", count)// 獲取第一個段落的文本
firstParagraph := paragraphs.First().Text()
fmt.Printf("第一個段落內容: %s\n", firstParagraph)
Length()
告訴我們找到了多少個元素First()
取出第一個找到的元素
3.4.2 Text方法 - 獲取文本內容
// 獲取h1標簽的文本
title := doc.Find("h1").Text()
fmt.Println("標題文本:", title)
// 輸出: 標題文本: 歡迎來到我的網站
Text()
方法提取元素內的所有文本,包括子元素的文本- 它會自動去除HTML標簽,只保留純文本
- 它會合并所有文本節點,中間可能有空格
3.4.3 Html方法 - 獲取HTML內容
// 獲取元素的HTML
contentHtml, err := doc.Find(".content").Html()
if err == nil {fmt.Println("內容區HTML:")fmt.Println(contentHtml)// 輸出會包含所有HTML標簽和內容
}
Html()
方法獲取元素的完整HTML代碼,包括所有標簽- 如果你需要保留原始格式,比如需要分析HTML結構,這很有用
3.4.4 Attr方法 - 獲取屬性
// 獲取鏈接的href屬性
doc.Find("a").Each(func(i int, s *goquery.Selection) {// Attr返回兩個值:屬性值和是否存在該屬性href, exists := s.Attr("href")if exists {fmt.Printf("鏈接 #%d 指向: %s\n", i+1, href)// 獲取鏈接文本text := s.Text()fmt.Printf("鏈接文本: %s\n", text)}
})
s.Attr("href")
嘗試獲取元素的href
屬性- 它返回兩個值:屬性的值和一個布爾值表示屬性是否存在
exists
告訴我們屬性是否存在,防止我們使用不存在的屬性
3.4.5 Each方法 - 遍歷所有元素
剛剛我們獲取quotes的例子中,我們想要獲取到頁面所有的.text
和.author
元素,Each是一個不二的選擇,簡單的修改下代碼:
// 從響應創建文檔doc, err := goquery.NewDocumentFromReader(resp.Body)if err != nil {fmt.Println("解析HTML失敗:", err)return}// 使用文檔doc.Find(`.quote`).Each(func(i int, s *goquery.Selection) {quote := s.Find(".text").Text()author := s.Find(".author").Text()fmt.Printf("第%d個: \n", i)fmt.Println("引言:", quote)fmt.Println("作者:", author)fmt.Println()})
Each
方法就像是一個循環,會依次處理每個找到的元素,- 函數
func(i int, s *goquery.Selection)
中:i
是當前處理的是第幾個元素(從0開始計數)s
就是當前正在處理的元素, 示例代碼中s
代表.quote
及下面的子元素
3.4.6 篩選元素方法**
篩選元素的方法有很多個,我一起介紹
items := doc.Find("li")// 獲取第一個元素
first := items.First()
fmt.Println("第一個菜單項:", first.Text())// 獲取最后一個元素
last := items.Last()
fmt.Println("最后一個菜單項:", last.Text())// 獲取特定索引的元素(從0開始)
second := items.Eq(1) // 第二個元素
fmt.Println("第二個菜單項:", second.Text())// 過濾有特定類的元素
selected := items.Filter(".selected")
fmt.Println("選中的菜單項:", selected.Text())// 排除特定元素
notSelected := items.Not(".selected")
fmt.Printf("未選中的菜單項有%d個\n", notSelected.Length())
First()
:拿出第一個元素Last()
:拿出最后一個元素Eq(1)
:拿出索引為1的元素(實際上是第2個,因為索引從0開始)Filter(".selected")
:只保留有class="selected"
的元素Not(".selected")
:排除有class="selected"
的元素,只留下其他的
3.5 完整實例:圖書信息提取
我們來寫一個完整的實例,從一個包含圖書信息的HTML頁面中提取圖書名稱、作者、出版年份、簡介、評分和鏈接。
package mainimport ("fmt""github.com/PuerkitoBio/goquery""strings"
)func main() {// 一個包含各種元素的HTML示例html := `<!DOCTYPE html><html><head><title>我的圖書列表</title></head><body><div id="header"><h1>我收藏的圖書</h1><p>這是我最喜歡的一些書籍</p></div><div class="book-list"><div class="book"><h2 class="title">Go語言編程</h2><p class="author">作者: 張三</p><p class="year">出版年份: 2022</p><p class="description">這是一本關于<b>Go語言</b>的入門書籍</p><span class="rating">評分: 4.5/5</span><a href="https://example.com/go-book" class="link">查看詳情</a></div><div class="book"><h2 class="title">Python數據分析</h2><p class="author">作者: 李四</p><p class="year">出版年份: 2021</p><p class="description">這本書講解了Python在<b>數據分析</b>中的應用</p><span class="rating">評分: 4.8/5</span><a href="https://example.com/python-book" class="link">查看詳情</a></div><div class="book"><h2 class="title">JavaScript高級編程</h2><p class="author">作者: 王五</p><p class="year">出版年份: 2023</p><p class="description">深入講解<b>JavaScript</b>的高級特性</p><span class="rating">評分: 4.2/5</span><a href="https://example.com/js-book" class="link">查看詳情</a></div><div class="book"><h2 class="title">Python入門</h2><p class="author"></p><p class="year">出版年份: 2023</p><p class="description">深入講解<b>Python</b>的高級特性</p><span class="rating">評分: 4.3/5</span><a href="https://example.com/js-book" class="link">查看詳情</a></div></div><div id="footer"><p>更新時間: 2025年3月15日</p></div></body></html>`// 創建goquery文檔reader := strings.NewReader(html)doc, err := goquery.NewDocumentFromReader(reader)if err != nil {fmt.Println("解析HTML失敗:", err)return}// 1. 提取頁面標題fmt.Println("=== 頁面信息 ===")pageTitle := doc.Find("title").Text()headerTitle := doc.Find("#header h1").Text()fmt.Printf("頁面標題: %s\n", pageTitle)fmt.Printf("主標題: %s\n", headerTitle)// 2. 提取所有圖書信息fmt.Println("\n=== 圖書列表 ===")doc.Find(".book").Each(func(i int, book *goquery.Selection) {// 提取圖書標題title := book.Find(".title").Text()// 提取作者(使用替代方法)authorElem := book.Find(".author")author := authorElem.Text()// 清理"作者: "前綴author = strings.TrimPrefix(author, "作者: ")// 提取評分(使用屬性選擇器)ratingText := book.Find(".rating").Text()// 使用strings包處理字符串rating := strings.TrimPrefix(ratingText, "評分: ")// 提取鏈接URL和文本linkElem := book.Find(".link")linkText := linkElem.Text()linkHref, _ := linkElem.Attr("href")// 輸出圖書信息fmt.Printf("圖書 #%d:\n", i+1)fmt.Printf(" 標題: %s\n", title)fmt.Printf(" 作者: %s\n", author)fmt.Printf(" 評分: %s\n", rating)fmt.Printf(" 鏈接: %s (%s)\n", linkText, linkHref)// 檢查描述中是否有強調內容desc := book.Find(".description")boldText := desc.Find("b").Text()if boldText != "" {fmt.Printf(" 重點內容: %s\n", boldText)}fmt.Println() // 添加空行分隔不同圖書})// 3. 統計信息fmt.Println("=== 統計信息 ===")bookCount := doc.Find(".book").Length()fmt.Printf("圖書總數: %d本\n", bookCount)// 統計高評分(>4.5)的書籍highRatedBooks := 0doc.Find(".book").Each(func(i int, s *goquery.Selection) {ratingText := s.Find(".rating").Text()// 提取評分數字ratingStr := strings.TrimPrefix(ratingText, "評分: ")ratingStr = strings.TrimSuffix(ratingStr, "/5")// 簡單轉換為浮點數進行比較var rating float64fmt.Sscanf(ratingStr, "%f", &rating)if rating > 4.5 {highRatedBooks++}})fmt.Printf("高評分圖書(>4.5): %d本\n", highRatedBooks)// 獲取頁腳信息footerText := doc.Find("#footer").Text()fmt.Printf("頁腳信息: %s\n", strings.TrimSpace(footerText))fmt.Println("=== 分隔符 ===")// 4. 查找并修改元素 ,查找作者為空的元素,填充作者doc.Find(".author:empty").SetHtml(`老六`)//查找.book ,第四個元素newSelection := doc.Find(".book").Eq(3)title := newSelection.Find(".title").Text()// 提取作者(使用替代方法)authorElem := newSelection.Find(".author")author := authorElem.Text()// 清理"作者: "前綴author = strings.TrimPrefix(author, "作者: ")// 提取評分(使用屬性選擇器)ratingText := newSelection.Find(".rating").Text()// 使用strings包處理字符串rating := strings.TrimPrefix(ratingText, "評分: ")// 提取鏈接URL和文本linkElem := newSelection.Find(".link")linkText := linkElem.Text()linkHref, _ := linkElem.Attr("href")// 輸出圖書信息fmt.Printf(" 標題: %s\n", title)fmt.Printf(" 作者: %s\n", author)fmt.Printf(" 評分: %s\n", rating)fmt.Printf(" 鏈接: %s (%s)\n", linkText, linkHref)
}
3.6 性能優化
- 使用具體的選擇器:避免使用過于寬泛的選擇器
- 緩存選擇結果:如果需要多次使用同一選擇器,先保存結果
- 避免深層嵌套:盡量使用直接的選擇器路徑
四、XPath查詢
XPath(XML Path Language)是一種用于在 XML 和 HTML 文檔中定位特定節點的查詢語言。它最初設計用于 XML 文檔,但由于 HTML 可以視為 XML 的一種變體,因此 XPath 也廣泛應用于 HTML 解析場景,尤其是在網絡爬蟲中用于提取特定數據。?
XPath 使用類似文件系統路徑的語法來描述節點在文檔中的位置,支持從根節點開始的絕對路徑查詢,也支持從當前節點開始的相對路徑查詢。在爬蟲領域,XPath 常用于從 HTML 頁面中提取結構化數據,如新聞標題、商品價格、評論內容等。
4.1 XPath 核心語法規則
4.1.1 基本路徑表達式
XPath 使用以下符號構建路徑表達式:
/
- 從根節點選擇//
- 從當前節點選擇文檔中符合條件的所有節點.
- 選擇當前節點..
- 選擇當前節點的父節點@
- 選擇屬性
4.1.2 節點選擇器
XPath 提供多種節點選擇器用于定位特定節點:
- 標簽名選擇:直接使用標簽名選擇節點,如
div
、a
- 通配符選擇:
*
表示選擇所有節點,@*
表示選擇所有屬性 - 節點索引:使用方括號
[]
指定節點索引,如div[1]
表示第一個 div 節點 - 屬性選擇:通過屬性名和值選擇節點,如
a[@href]
表示有 href 屬性的 a 節點,a[@class='link']
表示 class 屬性為 ‘link’ 的 a 節點
4.1.3 常用操作符
XPath 支持多種操作符用于構建復雜查詢:
- 邏輯操作符:
and
、or
、not()
- 比較操作符:
=
、!=
、<
、>
、<=
、>=
- 算術操作符:
+
、-
、*
、div
- 位置操作符:
start-with()
、contains()
、text()
4.1.4 XPath常用表達式示例
- 謂語(篩選條件)
//li[1]
- 選擇第一個li元素//li[last()]
- 選擇最后一個li元素//div[count(p) > 2]
- 選擇包含超過2個段落的div元素
- 軸(指定節點關系方向)
//h1/following-sibling::p
- 選擇h1后的所有兄弟段落//li/ancestor::div
- 選擇li的所有div祖先元素//a/parent::div
- 選擇a的父元素中的div
- 函數
string(//h1)
- 獲取第一個h1元素的文本contains(//p, '文本')
- 檢查段落是否包含"文本"count(//li)
- 計算li元素的數量
五、在Go中使用XPath:htmlquery
goquery也是支持xpath的,但是為了讓大家了解更多的庫,這里我們使用htmlquery
庫來解析,htmlquery
更專注于xpath的解析
5.1 安裝htmlquery
go get github.com/antchfx/htmlquery
5.2 簡單示例
package mainimport ("fmt""log""strings""github.com/antchfx/htmlquery""golang.org/x/net/html"
)func xpathExamples() {htmlStr := `<books><book id="1" category="fiction"><title>Go編程</title><author>作者1</author><price>59.90</price></book><book id="2" category="technical"><title>數據結構</title><author>作者2</author><price>79.90</price></book><book id="3" category="fiction"><title>算法導論</title><author>作者3</author><price>99.90</price></book></books>`doc, err := html.Parse(strings.NewReader(htmlStr))if err != nil {log.Fatal(err)}// 1. 基本路徑表達式fmt.Println("=== 基本XPath ===")// 選擇所有書的標題titles := htmlquery.Find(doc, "//title")for _, title := range titles {fmt.Printf("Title: %s\n", htmlquery.InnerText(title))}// 2. 屬性選擇fmt.Println("\n=== 屬性選擇 ===")// 選擇category為fiction的書fictionBooks := htmlquery.Find(doc, "//book[@category='fiction']/title")for _, book := range fictionBooks {fmt.Printf("Fiction book: %s\n", htmlquery.InnerText(book))}// 3. 位置選擇fmt.Println("\n=== 位置選擇 ===")// 選擇第一本書firstBook := htmlquery.FindOne(doc, "//book[1]/title")if firstBook != nil {fmt.Printf("First book: %s\n", htmlquery.InnerText(firstBook))}// 選擇最后一本書lastBook := htmlquery.FindOne(doc, "//book[last()]/title")if lastBook != nil {fmt.Printf("Last book: %s\n", htmlquery.InnerText(lastBook))}// 4. 條件表達式fmt.Println("\n=== 條件表達式 ===")// 價格大于60的書expensiveBooks := htmlquery.Find(doc, "//book[price>60]/title")for _, book := range expensiveBooks {fmt.Printf("Expensive book: %s\n", htmlquery.InnerText(book))}// 5. 軸運算fmt.Println("\n=== 軸運算 ===")// 選擇作者為"作者2"的書的下一個兄弟節點nextBook := htmlquery.FindOne(doc, "//author[text()='作者2']/parent::book/following-sibling::book[1]/title")if nextBook != nil {fmt.Printf("Next book after 作者2's book: %s\n", htmlquery.InnerText(nextBook))}
}
六、goquery與htmlquery比較
6.1基本實現對比
goquery:
- 靈感來源:基于jQuery的API設計
- 查詢語法:主要使用CSS選擇器
- 底層實現:基于Go標準庫的
net/html
包 - 鏈式操作:支持jQuery風格的鏈式調用
htmlquery:
- 靈感來源:專注于XPath查詢
- 查詢語法:主要使用XPath表達式
- 底層實現:同樣基于
net/html
包,但專門優化了XPath支持 - 函數式操作:提供函數式的查詢API
6.2 查詢語法對比
功能 | goquery (CSS選擇器) | htmlquery (XPath) |
---|---|---|
選擇所有div | doc.Find("div") | htmlquery.Find(doc, "//div") |
按class選擇 | doc.Find(".content") | htmlquery.Find(doc, "//[@class='content']") |
按ID選擇 | doc.Find("#title") | htmlquery.Find(doc, "//*[@id='title']") |
選擇第一個元素 | doc.Find("p").First() | htmlquery.FindOne(doc, "//p[1]") |
選擇最后一個元素 | doc.Find("p").Last() | htmlquery.FindOne(doc, "//p[last()]") |
包含文本 | doc.Find("p:contains('文本')") | htmlquery.Find(doc, "//p[contains(text(),'文本')]") |
父子關系 | doc.Find("div > p") | htmlquery.Find(doc, "//div/p") |
祖先后代 | doc.Find("div p") | htmlquery.Find(doc, "//div//p") |
6.3 性能對比
goquery:
- 優勢:CSS選擇器解析較快,鏈式操作減少重復查詢
- 劣勢:復雜查詢可能需要多次調用
htmlquery:
- 優勢:XPath查詢功能強大,一次查詢可以完成復雜條件
- 劣勢:XPath解析相對較慢
6.4 適用場景
使用goquery的場景:
- 前端開發背景:熟悉jQuery或CSS選擇器
- 簡單到中等復雜度的查詢:大部分網頁爬蟲需求
- 需要DOM操作:修改、添加、刪除元素
- 鏈式操作偏好:喜歡流暢的API調用
- 快速原型開發:語法簡潔,開發效率高
使用htmlquery的場景:
- 復雜查詢需求:需要使用XPath的高級功能
- XML處理經驗:熟悉XPath語法
- 精確節點定位:需要基于位置、文本內容等復雜條件查詢
- 性能敏感:單次復雜查詢比多次簡單查詢更高效
- 數據提取為主:主要用于讀取,不需要修改DOM
實際項目建議:對于大多數網頁爬蟲項目,建議優先選擇goquery,只有在遇到goquery無法高效解決的復雜查詢時,再考慮使用htmlquery作為補充。
七、處理中文編碼問題
7.1 編碼類型
在網絡傳輸中,數據通常以二進制形式進行編碼,而不同的編碼方式會導致數據的顯示或處理方式不同。在爬取中文網站時,經常會遇到編碼問題。常見的中文編碼包括:
- UTF-8:Unicode的一種編碼方式,支持全球幾乎所有字符,是最常用的編碼方式。
- GBK/GB2312:中國的編碼方式,主要用于簡體中文,兼容ASCII字符集。
- BIG5:臺灣的編碼方式,主要用于繁體中文。
7.2 編碼檢測
在爬取網頁時,我們通常無法確定網頁的編碼類型,因此需要進行編碼檢測。以下是一些常用的編碼檢測方法:
-
HTTP頭信息:大多數網頁會在HTTP頭信息中包含
Content-Type
字段,其中包含了網頁的編碼類型。 -
HTML文檔:可以通過查看HTML文檔的頭部部分,查找
<meta>
標簽中charset
屬性的值。 -
字符識別:可以使用一些字符識別工具,如
chardet
庫,自動檢測網頁的編碼類型。
7.3 處理中文編碼
下面使用一個姓名評分的網站來演示,網站的編碼是GBK,我們來演示下編碼處理:
package mainimport ("bytes""fmt""golang.org/x/text/encoding/simplifiedchinese""golang.org/x/text/transform""io""net/http""strings""github.com/PuerkitoBio/goquery"
)// 處理中文編碼的HTTP客戶端
func fetchWithEncoding(url string) (*goquery.Document, error) {resp, err := http.Get(url)if err != nil {return nil, err}defer resp.Body.Close()// 讀取響應內容body, err := io.ReadAll(resp.Body)if err != nil {return nil, err}// 檢測編碼類型// bodyStr := string(body)var reader io.Reader// 檢查是否是GB2312/GBK編碼if detectEncoding(body) == "gbk" {reader = transform.NewReader(bytes.NewReader(body), simplifiedchinese.GBK.NewDecoder())} else {reader = bytes.NewReader(body)}/*if strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "gb2312") ||strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "gbk") ||strings.Contains(strings.ToLower(bodyStr), "gb2312") ||strings.Contains(strings.ToLower(bodyStr), "gbk") {// 轉換GBK到UTF-8reader = transform.NewReader(bytes.NewReader(body), simplifiedchinese.GBK.NewDecoder())} else {// 默認使用UTF-8reader = bytes.NewReader(body)}*/// 解析HTMLdoc, err := goquery.NewDocumentFromReader(reader)if err != nil {return nil, err}return doc, nil
}// 自動檢測編碼
func detectEncoding(body []byte) string {bodyStr := strings.ToLower(string(body))// 檢查HTML中的編碼聲明if strings.Contains(bodyStr, "charset=gb2312") || strings.Contains(bodyStr, "charset=gbk") {return "gbk"}if strings.Contains(bodyStr, "charset=utf-8") {return "utf-8"}// 默認返回UTF-8return "utf-8"
}func main() {// 使用示例doc, err := fetchWithEncoding("http://www.8882088.com/ceming/result.php?firstname=%C1%FA&lastname=%B0%C1%CC%EC&xb=0&bir_year=2025&bir_month=5&bir_day=28&bir_hour=5")if err != nil {fmt.Printf("獲取頁面失敗: %v\n", err)return}// 正常使用goquery處理中文內容title := doc.Find("title").Text()fmt.Printf("頁面標題: %s\n", title)
}