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)
}
})
最佳实践
- 错误处理: 始终检查 NewDocumentFromReader 的错误
- 资源清理: 确保关闭 HTTP 响应体
- 缓存策略: 对频繁访问的页面实现缓存
- 超时控制: 为 HTTP 请求设置合理的超时
- 去重处理: 避免重复解析相同内容
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✅ 爬虫任务完成!")
}