Go語言爬蟲系列教程5:HTML解析技術以及第三方庫選擇

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 選擇器優先級

當多個選擇器作用于同一元素時,優先級規則如下(從高到低):

  1. 內聯樣式style="..."
  2. ID選擇器#id
  3. 類選擇器、屬性選擇器、偽類.class[attr]:hover
  4. 元素選擇器、偽元素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 性能優化

  1. 使用具體的選擇器:避免使用過于寬泛的選擇器
  2. 緩存選擇結果:如果需要多次使用同一選擇器,先保存結果
  3. 避免深層嵌套:盡量使用直接的選擇器路徑

四、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 提供多種節點選擇器用于定位特定節點:

  • 標簽名選擇:直接使用標簽名選擇節點,如diva
  • 通配符選擇:*表示選擇所有節點,@*表示選擇所有屬性
  • 節點索引:使用方括號[]指定節點索引,如div[1]表示第一個 div 節點
  • 屬性選擇:通過屬性名和值選擇節點,如a[@href]表示有 href 屬性的 a 節點,a[@class='link']表示 class 屬性為 ‘link’ 的 a 節點
4.1.3 常用操作符

XPath 支持多種操作符用于構建復雜查詢:

  • 邏輯操作符:andornot()
  • 比較操作符:=!=<><=>=
  • 算術操作符:+-*div
  • 位置操作符:start-with()contains()text()
4.1.4 XPath常用表達式示例
  1. 謂語(篩選條件)
  • //li[1] - 選擇第一個li元素
  • //li[last()] - 選擇最后一個li元素
  • //div[count(p) > 2] - 選擇包含超過2個段落的div元素
  1. 軸(指定節點關系方向)
  • //h1/following-sibling::p - 選擇h1后的所有兄弟段落
  • //li/ancestor::div - 選擇li的所有div祖先元素
  • //a/parent::div - 選擇a的父元素中的div
  1. 函數
  • 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)
選擇所有divdoc.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的場景:

  1. 前端開發背景:熟悉jQuery或CSS選擇器
  2. 簡單到中等復雜度的查詢:大部分網頁爬蟲需求
  3. 需要DOM操作:修改、添加、刪除元素
  4. 鏈式操作偏好:喜歡流暢的API調用
  5. 快速原型開發:語法簡潔,開發效率高

使用htmlquery的場景:

  1. 復雜查詢需求:需要使用XPath的高級功能
  2. XML處理經驗:熟悉XPath語法
  3. 精確節點定位:需要基于位置、文本內容等復雜條件查詢
  4. 性能敏感:單次復雜查詢比多次簡單查詢更高效
  5. 數據提取為主:主要用于讀取,不需要修改DOM

實際項目建議:對于大多數網頁爬蟲項目,建議優先選擇goquery,只有在遇到goquery無法高效解決的復雜查詢時,再考慮使用htmlquery作為補充。

七、處理中文編碼問題

7.1 編碼類型

在網絡傳輸中,數據通常以二進制形式進行編碼,而不同的編碼方式會導致數據的顯示或處理方式不同。在爬取中文網站時,經常會遇到編碼問題。常見的中文編碼包括:

  1. UTF-8:Unicode的一種編碼方式,支持全球幾乎所有字符,是最常用的編碼方式。
  2. GBK/GB2312:中國的編碼方式,主要用于簡體中文,兼容ASCII字符集。
  3. BIG5:臺灣的編碼方式,主要用于繁體中文。

7.2 編碼檢測

在爬取網頁時,我們通常無法確定網頁的編碼類型,因此需要進行編碼檢測。以下是一些常用的編碼檢測方法:

  1. HTTP頭信息:大多數網頁會在HTTP頭信息中包含Content-Type字段,其中包含了網頁的編碼類型。

  2. HTML文檔:可以通過查看HTML文檔的頭部部分,查找<meta>標簽中charset屬性的值。

  3. 字符識別:可以使用一些字符識別工具,如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)
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/83710.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/83710.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/83710.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

AtCoder 第408?場初級競賽 A~E題解

A Timeout 【題目鏈接】 原題鏈接:A - Timeout 【考點】 模擬 【題目大意】 長老會在 s 秒后睡去,進過 n 次叫醒,長老最后能否是保持清醒。 【解析】 模擬每一次拍擊叫醒的過程,查看本次時間距上次時間是否大于 s。注意:第一次拍擊叫醒應和 0 秒相減。 【難度】 …

Unity VR/MR開發-VR設備與適用場景分析

視頻講解鏈接&#xff1a;【XR馬斯維】VR/MR設備與適用場景分析&#xff1f;【UnityVR/MR開發教程--入門】_游戲熱門視頻

MyBatis 查詢功能實現全流程

一、創建maven項目 配置好相應的jdk 二、在數據庫建立相應的表格 1.因為Mybatis實際是對sql表的一系列操作&#xff0c;所以我們新建一個數據庫 2.在查詢界面運行下面指令創建一個user表 CREATE TABLE user (id int(11) NOT NULL AUTO_INCREMENT,username varchar(32) NOT NU…

tcp/udp

tcp/udp協議概述 傳輸層協議基本概念 傳輸層協議建立在網絡層和會話層之間&#xff0c;為應用層實體提供端到端的通信功能&#xff0c;確保數據包的順序傳送及數據的完整性。它利用網絡層提供的服務&#xff0c;并通過傳輸層地址&#xff08;端口號&#xff09;提供給高層用戶…

k8s集群安裝坑點匯總

前言 由于使用最新的Rocky9.5,導致kubekey一鍵安裝用不了&#xff0c;退回Rocky8麻煩機器都建好了&#xff0c;決定手動安裝k8s&#xff0c;結果手動安裝過程中遇到各種坑&#xff0c;這里記錄下&#xff1b; k8s安裝 k8s具體安裝過程可自行搜索&#xff0c;或者deepseek; 也…

深入解析 Dotnet-Boxed.Framework:提升 .NET 開發效率的利器

在現代 .NET 開發中&#xff0c;框架和工具的選擇對項目的開發效率和長期維護至關重要。Dotnet-Boxed.Framework 是一個開源框架&#xff0c;旨在簡化開發流程&#xff0c;提高生產力。它通過一組實用的工具和自動化功能&#xff0c;幫助開發者快速構建高質量的應用程序。本文將…

如何輕松地將文件從 PC 傳輸到 iPhone?

傳統上&#xff0c;您可以使用 iTunes 將文件從 PC 傳輸到 iPhone&#xff0c;但現在&#xff0c;使用 iTunes 已不再是唯一的選擇。現在有多種不同且有效的方法可以幫助您傳輸文件。在今天的指南中&#xff0c;您可以找到 8 種使用或不使用 iTunes 傳輸文件的方法&#xff0c;…

Kafka深度解析與原理剖析

文章目錄 一、Kafka核心架構原理1. **分布式協調與選舉**2. **ISR、OSR與HW機制**3. **高性能存儲設計**4. **刷盤機制 (Flush)**5. **消息壓縮算法**二、高可用與消息可靠性保障1. **數據高可用策略**2. **消息丟失場景與規避**3. **順序消費保證**三、Kafka高頻面試題精析1. …

【教學類】20250605立體紙盤(3邊形-22邊形,角度5、10……40,45)

背景需求 在《自助餐》活動中&#xff0c; 【教學類-53-01】20240918自助餐餐盤-CSDN博客文章瀏覽閱讀984次&#xff0c;點贊29次&#xff0c;收藏11次。【教學類-53-01】20240918自助餐餐盤https://blog.csdn.net/reasonsummer/article/details/142340542?spm1011.2415.300…

GC1809:高性能24bit/192kHz音頻接收芯片解析

1. 芯片概述 GC1809 是數字音頻接收芯片&#xff0c;支持IEC60958、S/PDIF、AES3等協議&#xff0c;集成8選1輸入切換、低抖動時鐘恢復和24bit DAC&#xff0c;適用于家庭影院、汽車音響等高保真場景。 核心特性 高精度&#xff1a;24bit分辨率&#xff0c;動態范圍105dB&…

Next.js 中間件鑒權繞過漏洞 CVE-2025-29927

前言:CVE-2025-29927 是一個影響 Next.js 的嚴重漏洞&#xff0c;源于開發者信任了客戶端請求中攜帶的 X-Middleware-Rewrite 頭部字段。攻擊者可以手動構造該頭部&#xff0c;實現繞過中間件邏輯&#xff0c;訪問本應受保護的資源或 API。 影響版本&#xff1a;Next.js < …

第1章 數據分析簡介

第1章 數據分析簡介 1.1 數據分析 當今世界對信息技術依賴日深,每天產生和存儲海量數據,來源于自動檢測系統、傳感器、科學儀器,以及銀行取錢、買東西、寫博客、發微博等日常行為。 數據與信息在形式上不同:數據是無形式可言的字節流,難理解其本質;信息是對數據集處理后…

邊緣計算網關賦能沸石轉輪運行故障智能診斷的配置實例

一、項目背景 在環保行業&#xff0c;隨著國家對大氣污染治理要求的不斷提高&#xff0c;VOCs廢氣處理成為了眾多企業的重要任務。沸石轉輪作為一種高效的VOCs治理設備&#xff0c;被廣泛應用于石油化工、汽車制造、印刷包裝等主流行業。這些行業生產規模大、廢氣排放量多&…

20250602在Ubuntu20.04.6下修改壓縮包的日期和時間

rootrootrootroot-X99-Turbo:~$ ll -rwxrwxrwx 1 rootroot rootroot 36247187308 5月 23 10:23 Android13.0地面站.tgz* rootrootrootroot-X99-Turbo:~$ touch 1Android13.0地面站.tgz rootrootrootroot-X99-Turbo:~$ ll -rwxrwxrwx 1 rootroot rootroot 36247187308 6月…

windows無法安裝到這個磁盤,選中的磁盤采用gpt分區儀式

解決辦法&#xff1a; 我才用的是一個網友分享的微軟官方解決辦法&#xff0c;成功了&#xff0c;但是不知道會不會i有什么影響。將所有分區刪掉&#xff0c;這時磁盤變成為分配的空間。我個人是兩塊固態&#xff0c;一塊m.2&#xff0c;一塊sata&#xff1b;所以我直接將500g…

使用Composer創建公共類庫

概述 如果多個項目中存在使用相同類庫、模塊的情況&#xff0c;此時可以考慮將類庫或者模塊單獨抽取出來&#xff0c;形成獨立類庫&#xff0c;通過composer 來進行依賴管理&#xff0c;這樣可以更方便維護&#xff0c;大大提升開發效率。 優勢 可以對特定模塊進行統一維護和…

Ubuntu中TFTP服務器安裝使用

TFTP服務器 在 Ubuntu 下使用 TFTP&#xff08;Trivial File Transfer Protocol&#xff09; 服務&#xff0c;通常用于簡單的文件傳輸&#xff08;如網絡設備固件更新、嵌入式開發等&#xff09;。 1 TFTP服務器安裝 sudo apt-get install tftp-hpa sudo apt-get install…

前端面試總結

1.請做下自我介紹 技術棧 工作經歷 2.挑一個項目詳細講一下 介紹了一個項目的業務&#xff0c;重點講了一個動態表單的實現&#xff0c;從業務、擴展性、可維護性、性能提升 3.場景題&#xff1a;問答怎么做&#xff0c;表單之間關聯&#xff0c;回答不同問題跳轉到不同的下一個…

Python訓練打卡Day38

Dataset和Dataloader類 知識點回顧&#xff1a; Dataset類的__getitem__和__len__方法&#xff08;本質是python的特殊方法&#xff09;Dataloader類minist手寫數據集的了解 在遇到大規模數據集時&#xff0c;顯存常常無法一次性存儲所有數據&#xff0c;所以需要使用分批訓練的…

web3-區塊鏈基礎:從區塊添加機制到哈希加密與默克爾樹結構

區塊鏈基礎&#xff1a;從區塊添加機制到哈希加密與默克爾樹結構 什么是區塊鏈 抽象的回答: 區塊鏈提供了一種讓多個參與方在沒有一個唯一可信方的情況下達成合作 若有可信第三方 > 不需要區塊鏈 [金融系統中常常沒有可信的參與方] 像股票市場&#xff0c;或者一個國家的…