三十的博客

GoQuery 学习

本文内容基于 AI 生成结果整理,可能包含不准确信息,仅供参考使用。
发布时间
阅读量 加载中...

GoQuery 是 Go 语言的 jQuery 风格 HTML 解析库,可以让你像操作 DOM 一样轻松提取网页数据。

安装

bash
go get github.com/PuerkitoBio/goquery

基础用法

初始化文档

go
import "github.com/PuerkitoBio/goquery"

// 从字符串创建
html := `<html><body><div class="content">Hello</div></body></html>`
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))

// 从 URL 创建(自动发起HTTP请求)
doc, err := goquery.NewDocument("http://example.com")

// 从 *http.Response 创建
resp, _ := http.Get("http://example.com")
doc, err := goquery.NewDocumentFromResponse(resp)

基本选择器

go
// 类选择器
doc.Find(".content").Each(func(i int, s *goquery.Selection) {
    fmt.Println(s.Text()) // 输出: Hello
})

// ID选择器
doc.Find("#main").Text()

// 元素选择器
doc.Find("div").First().Html()

// 属性选择器
doc.Find("a[href]")       // 所有带href的<a>标签
doc.Find("img[src$=.png]") // src以.png结尾的图片

选择器进阶

组合选择器

go
// 后代选择器
doc.Find("div p") // div下的所有p元素

// 子元素选择器
doc.Find("ul > li") // ul的直接子li元素

// 相邻兄弟选择器
doc.Find("h1 + p") // 紧接在h1后的p元素

// 通用兄弟选择器
doc.Find("h2 ~ p") // h2后面的所有同级p元素

伪类选择器

go
doc.Find("li:first-child")  // 第一个li
doc.Find("tr:odd")          // 奇数行
doc.Find("a:contains(新闻)") // 包含"新闻"文本的链接
doc.Find("div:has(img)")    // 包含img的div

元素操作

获取内容

go
// 获取HTML内容
html, _ := doc.Find("div").Html()

// 获取文本(自动去除HTML标签)
text := doc.Find("title").Text()

// 获取属性
href, exists := doc.Find("a").Attr("href")
if exists {
    fmt.Println(href)
}

// 获取多个元素
doc.Find("a").Each(func(i int, s *goquery.Selection) {
    fmt.Printf("%d: %s\n", i, s.Text())
})

遍历元素

go
// 遍历选择结果
doc.Find("li").EachWithBreak(func(i int, s *goquery.Selection) bool {
    if i == 5 {
        return false // 停止遍历
    }
    fmt.Println(s.Text())
    return true // 继续遍历
})

// 链式调用
doc.Find("div.content").
    Find("a").
    Filter(":contains(下载)").
    Each(func(i int, s *goquery.Selection) {
        // 处理每个符合条件的链接
    })

实战案例

抓取新闻标题和链接

go
doc, _ := goquery.NewDocument("https://news.baidu.com")

var news []struct {
    Title string
    URL   string
}

doc.Find(".hotnews a").Each(func(i int, s *goquery.Selection) {
    title := s.Text()
    url, _ := s.Attr("href")

    news = append(news, struct{
        Title string
        URL   string
    }{
        Title: strings.TrimSpace(title),
        URL:   url,
    })
})

fmt.Printf("%+v", news)

提取表格数据

html
<!-- 假设网页中有如下表格 -->
<table id="data">
  <tr>
    <th>姓名</th>
    <th>年龄</th>
  </tr>
  <tr>
    <td>张三</td>
    <td>25</td>
  </tr>
  <tr>
    <td>李四</td>
    <td>30</td>
  </tr>
</table>
go
type Person struct {
    Name string
    Age  string
}

var people []Person

doc.Find("#data tr").Each(func(i int, s *goquery.Selection) {
    // 跳过表头
    if i == 0 {
        return
    }

    name := s.Find("td").Eq(0).Text()
    age := s.Find("td").Eq(1).Text()

    people = append(people, Person{
        Name: strings.TrimSpace(name),
        Age:  strings.TrimSpace(age),
    })
})

高级技巧

处理相对链接

go
baseURL := "https://example.com"

doc.Find("a[href]").Each(func(i int, s *goquery.Selection) {
    href, _ := s.Attr("href")

    // 转换相对URL为绝对URL
    absoluteURL, err := url.Parse(href)
    if err != nil {
        return
    }
    absoluteURL = baseURL.ResolveReference(absoluteURL)

    fmt.Println(absoluteURL.String())
})

处理动态加载的内容

对于 JavaScript 动态生成的内容,可以配合 chromedp 使用:

go
import "github.com/chromedp/chromedp"

// 先获取完整渲染后的HTML
var html string
err := chromedp.Run(ctx,
    chromedp.Navigate("https://example.com"),
    chromedp.WaitVisible("#dynamic-content"),
    chromedp.OuterHTML("html", &html),
)

// 再用goquery解析
doc, _ := goquery.NewDocumentFromReader(strings.NewReader(html))

性能优化技巧

go
// 1. 复用Document对象
var doc *goquery.Document

// 2. 预编译常用选择器
titleSel := goquery.NewDocumentMust("title")
linkSel := goquery.NewDocumentMust("a[href]")

// 3. 限制查找范围
content := doc.Find("#main-content") // 先缩小范围
content.Find("a").Each(...)         // 在范围内查找

// 4. 使用Selection的First/Last减少遍历
doc.Find("li").First().Text()

常见问题解决

处理特殊编码

go
// 自动检测编码
reader := transform.NewReader(resp.Body,
    simplifiedchinese.GBK.NewDecoder())
doc, err := goquery.NewDocumentFromReader(reader)

// 或者手动指定
html, _ := ioutil.ReadAll(resp.Body)
utf8html, _ := simplifiedchinese.GBK.NewDecoder().Bytes(html)
doc, _ := goquery.NewDocumentFromReader(bytes.NewReader(utf8html))

处理大文件

go
// 流式处理大HTML文件
f, _ := os.Open("large.html")
defer f.Close()

doc, _ := goquery.NewDocumentFromReader(io.LimitReader(f, 10<<20)) // 限制10MB

// 分块处理
doc.Find("div.item").Each(func(i int, s *goquery.Selection) {
    // 处理每个项目
    if i%100 == 0 {
        fmt.Printf("已处理 %d 个项目\n", i)
    }
})

最佳实践

go
// 完整示例
func ExtractDataFromURL(url string) ([]Data, error) {
    // 1. 获取HTML
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Get(url)
    if err != nil {
        return nil, fmt.Errorf("HTTP请求失败: %v", err)
    }
    defer resp.Body.Close()

    // 2. 处理编码
    utf8Reader := transform.NewReader(resp.Body,
        simplifiedchinese.GBK.NewDecoder())

    // 3. 解析文档
    doc, err := goquery.NewDocumentFromReader(utf8Reader)
    if err != nil {
        return nil, fmt.Errorf("HTML解析失败: %v", err)
    }

    // 4. 提取数据
    var results []Data
    doc.Find(".item").Each(func(i int, s *goquery.Selection) {
        title := s.Find("h3").Text()
        link, _ := s.Find("a").Attr("href")

        results = append(results, Data{
            Title: strings.TrimSpace(title),
            Link:  link,
        })
    })

    return results, nil
}

本地 HTML 项目实战

本地 HTML 文件

创建一个名为 demo.html 的文件:

html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>GoQuery学习页面 - 图书商城</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 40px;
      }
      .book {
        border: 1px solid #ddd;
        padding: 15px;
        margin: 10px;
        border-radius: 5px;
      }
      .price {
        color: #e74c3c;
        font-weight: bold;
      }
      .sale {
        background-color: #ffeaa7;
        padding: 3px 8px;
        border-radius: 3px;
      }
      .rating {
        color: #f39c12;
      }
    </style>
  </head>
  <body>
    <header>
      <h1>📚 编程图书商城</h1>
      <nav>
        <ul id="main-nav">
          <li><a href="#go-books">Go语言</a></li>
          <li><a href="#python-books">Python</a></li>
          <li><a href="#web-books">前端开发</a></li>
        </ul>
      </nav>
    </header>

    <main>
      <section id="go-books" class="book-section">
        <h2>Go语言图书专区</h2>
        <div class="book" data-category="go" data-id="1001">
          <h3 class="title">《Go语言实战》</h3>
          <p class="author">作者: 张三</p>
          <p class="price">价格: <span class="amount">69.90</span></p>
          <p class="rating">评分: ⭐⭐⭐⭐⭐ (4.8)</p>
          <div class="sale">热卖中</div>
        </div>

        <div class="book" data-category="go" data-id="1002">
          <h3 class="title">《Go Web编程》</h3>
          <p class="author">作者: 李四</p>
          <p class="price">价格: <span class="amount">89.00</span></p>
          <p class="rating">评分: ⭐⭐⭐⭐ (4.2)</p>
        </div>
      </section>

      <section id="python-books" class="book-section">
        <h2>Python图书专区</h2>
        <div class="book" data-category="python" data-id="2001">
          <h3 class="title">《Python数据分析》</h3>
          <p class="author">作者: 王五</p>
          <p class="price">价格: <span class="amount">79.00</span></p>
          <p class="rating">评分: ⭐⭐⭐⭐⭐ (4.9)</p>
          <div class="sale">新品上市</div>
        </div>
      </section>

      <section id="web-books" class="book-section">
        <h2>前端开发图书</h2>
        <div class="book" data-category="web" data-id="3001">
          <h3 class="title">《现代JavaScript教程》</h3>
          <p class="author">作者: 赵六</p>
          <p class="price">价格: <span class="amount">99.00</span></p>
          <p class="rating">评分: ⭐⭐⭐⭐ (4.5)</p>
        </div>
      </section>
    </main>

    <footer>
      <p>© 2024 编程图书商城. 所有权利保留.</p>
      <ul class="footer-links">
        <li><a href="/about">关于我们</a></li>
        <li><a href="/contact">联系我们</a></li>
        <li><a href="/privacy">隐私政策</a></li>
      </ul>
    </footer>
  </body>
</html>

Go 爬虫程序

go
package main

import (
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"

	"github.com/PuerkitoBio/goquery"
)

// 定义图书结构体
type Book struct {
	ID      string
	Title   string
	Author  string
	Price   float64
	Rating  float64
	Category string
	OnSale  bool
}

// 定义导航链接结构体
type NavLink struct {
	Text string
	Href string
}

func main() {
	// 检查文件是否存在
	if _, err := os.Stat("demo.html"); os.IsNotExist(err) {
		log.Fatal("请先创建 demo.html 文件!")
	}

	fmt.Println("🚀 开始解析本地HTML文件...")

	// 1. 从本地文件加载HTML
	file, err := os.Open("demo.html")
	if err != nil {
		log.Fatalf("无法打开文件: %v", err)
	}
	defer file.Close()

	doc, err := goquery.NewDocumentFromReader(file)
	if err != nil {
		log.Fatalf("解析HTML失败: %v", err)
	}

	fmt.Println("✅ HTML文件解析成功!")
	fmt.Println("\n" + strings.Repeat("=", 50))

	// 2. 提取页面标题
	pageTitle := doc.Find("title").Text()
	fmt.Printf("📖 页面标题: %s\n", pageTitle)

	// 3. 提取导航菜单
	fmt.Println("\n🔗 导航菜单:")
	var navLinks []NavLink
	doc.Find("#main-nav a").Each(func(i int, s *goquery.Selection) {
		text := strings.TrimSpace(s.Text())
		href, exists := s.Attr("href")
		if exists {
			navLinks = append(navLinks, NavLink{Text: text, Href: href})
			fmt.Printf("  %d. %s -> %s\n", i+1, text, href)
		}
	})

	// 4. 提取所有图书信息
	fmt.Println("\n📚 图书信息:")
	var books []Book
	doc.Find(".book").Each(func(i int, s *goquery.Selection) {
		book := Book{
			ID:       s.AttrOr("data-id", ""),
			Category: s.AttrOr("data-category", ""),
			Title:    strings.TrimSpace(s.Find(".title").Text()),
			Author:   strings.TrimSpace(s.Find(".author").Text()),
			OnSale:   s.Find(".sale").Length() > 0,
		}

		// 提取价格
		priceText := s.Find(".price .amount").Text()
		if price, err := strconv.ParseFloat(priceText, 64); err == nil {
			book.Price = price
		}

		// 提取评分(从"⭐⭐⭐⭐ (4.2)"中提取数字)
		ratingText := s.Find(".rating").Text()
		if strings.Contains(ratingText, "(") {
			start := strings.Index(ratingText, "(") + 1
			end := strings.Index(ratingText, ")")
			if start > 0 && end > start {
				ratingStr := ratingText[start:end]
				if rating, err := strconv.ParseFloat(ratingStr, 64); err == nil {
					book.Rating = rating
				}
			}
		}

		books = append(books, book)

		fmt.Printf("\n%d. %s\n", i+1, book.Title)
		fmt.Printf("   📍 分类: %s\n", book.Category)
		fmt.Printf("   👨‍💻 作者: %s\n", book.Author)
		fmt.Printf("   💰 价格: %.2f元\n", book.Price)
		fmt.Printf("   ⭐ 评分: %.1f\n", book.Rating)
		fmt.Printf("   🏷️  促销: %v\n", book.OnSale)
	})

	// 5. 提取页脚链接
	fmt.Println("\n🔗 页脚链接:")
	doc.Find(".footer-links a").Each(func(i int, s *goquery.Selection) {
		text := s.Text()
		href, _ := s.Attr("href")
		fmt.Printf("  %d. %s -> %s\n", i+1, text, href)
	})

	// 6. 统计信息
	fmt.Println("\n" + strings.Repeat("=", 50))
	fmt.Printf("📊 统计信息:\n")
	fmt.Printf("  图书总数: %d\n", len(books))

	// 按分类统计
	categoryCount := make(map[string]int)
	totalValue := 0.0
	for _, book := range books {
		categoryCount[book.Category]++
		totalValue += book.Price
	}

	for category, count := range categoryCount {
		fmt.Printf("  %s图书: %d本\n", category, count)
	}
	fmt.Printf("  图书总价值: %.2f元\n", totalValue)

	// 7. 查找特定条件的图书
	fmt.Println("\n🎯 特价图书(价格低于80元):")
	for _, book := range books {
		if book.Price < 80 {
			fmt.Printf("  💸 %s - %.2f元\n", book.Title, book.Price)
		}
	}

	fmt.Println("\n✅ 爬虫任务完成!")
}
#Goquery #爬虫 #Golang