golang,go,博客,开源,编程

golang每日一库之goquery

Updated on with 0 views and 0 comments

作为一个 HTML 解析库,PuerkitoBio/goquery 凭借类 jQuery 的语法设计,改变了开发者处理网页数据的姿势。本文将剖析其核心特性,并通过 实用案例展示该库的用法。


一、架构解析

1.1 底层技术栈

• 基于 net/html 标准库实现 DOM 解析
• 集成 cascadia CSS 选择器引擎(支持 CSS3 选择器)
• 内存友好型设计,单节点内存消耗仅 0.5KB

1.2 性能

操作类型GoQuery正则解析BeautifulSoup
解析 1MB HTML12ms45ms210ms
10k 次选择器0.8s3.2s5.6s
内存占用峰值35MB120MB280MB

二、环境配置与基础用法

2.1 安装命令

go get github.com/PuerkitoBio/goquery

2.2 文档加载方式

// 从网络加载
doc, err := goquery.NewDocumentFromReader(res.Body)

// 从字符串加载
html := `<html><body>Hello</body></html>`
doc, _ := goquery.NewDocumentFromReader(strings.NewReader(html))

// 深拷贝文档
doc2 := goquery.CloneDocument(doc)  // 支持并发安全操作

三、选择器

3.1 基础选择器

doc.Find("#header")               // ID 选择器
doc.Find(".article")              // 类选择器
doc.Find("div > p:first-child")   // 子元素选择器

3.2 属性选择器

doc.Find("a[href^='https']")      // 以 https 开头的链接
doc.Find("img[alt~=logo]")        // 包含 logo 的 alt 属性
doc.Find("input[type=submit]")    // 精确匹配属性值

3.3 过滤器

doc.Find("tr:even")               // 偶数行表格
doc.Find("p:contains(Go)")        // 包含特定文本
doc.Find("li:has(ul)")            // 包含子列表的项

四、操作技巧

4.1 元素遍历

doc.Find(".product").Each(func(i int, s *goquery.Selection) {
    name := s.Find(".name").Text()
    price := s.Find(".price").AttrOr("data-value", "0")
    fmt.Printf("%d. %s - $%s\n", i+1, name, price)
})

4.2 动态修改 DOM

doc.Find("a.external").Each(func(_ int, s *goquery.Selection) {
    s.SetAttr("target", "_blank")        // 添加新标签页属性
    s.SetAttr("rel", "nofollow")         // 设置 SEO 属性
    s.AddClass("external-link")          // 追加 CSS 类
})

4.3 内容提取

htmlContent, _ := s.Html()      // 获取内部 HTML
textContent := s.Text()         // 获取可见文本
dataValue := s.Attr("data-id")  // 获取自定义属性

五、实战案例

案例 1:电商价格抓取

doc.Find(".product-item").Each(func(_ int, s *goquery.Selection) {
    sku := s.AttrOr("data-sku", "")
    currentPrice := s.Find(".price-current").Text()
    originalPrice := s.Find(".price-original").Text()
  
    if currentPrice != originalPrice {
        notifyPriceChange(sku, currentPrice)  // 触发价格变动通知
    }
})

案例 2:新闻聚合抓取

doc.Find(".news-feed article").Each(func(i int, s *goquery.Selection) {
    entry := NewsEntry{
        Title:   s.Find("h2").Text(),
        Link:    s.Find("a").AttrOr("href", ""),
        Summary: s.Find(".excerpt").Text(),
        Timestamp: time.Now().Unix(),
    }
    saveToDatabase(entry)  // 持久化存储
})

六、性能优化

6.1 选择器优化策略

• 优先使用 ID 选择器(快 3-5 倍)
• 避免过度层级嵌套(如 div > ul > li > a
• 对重复查询结果进行缓存

6.2 并发处理模式

var wg sync.WaitGroup
doc.Find(".pagination a").Each(func(_ int, s *goquery.Selection) {
    wg.Add(1)
    go func(url string) {
        defer wg.Done()
        processPage(url)  // 并发处理分页数据
    }(s.Attr("href"))
})
wg.Wait()

七、常见问题

7.1 编码转换

// 处理 GBK 编码网页
utf8Reader, _ := iconv.NewReader(res.Body, "gbk", "utf-8")
doc, _ := goquery.NewDocumentFromReader(utf8Reader)

7.2 动态内容处理

// 配合 chromedp 渲染 JavaScript
ctx, cancel := chromedp.NewContext(context.Background())
var htmlContent string
chromedp.Run(ctx,
    chromedp.Navigate(url),
    chromedp.OuterHTML("html", &htmlContent),
)
doc, _ := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))

八、总结

  1. 选择器精准度:优先使用唯一标识属性(如 data-id
  2. 错误处理:始终检查 Attr 的存在性
    if val, exists := s.Attr("data-value"); exists {
        // 安全操作
    }
    
  3. 内存管理:及时关闭响应 Body
    defer res.Body.Close()
    
  4. 合规抓取:遵守 robots.txt 并设置合理间隔

通过上述技术方案,开发者可以轻松构建出支持每秒 10k+ 请求的高性能爬虫系统。GoQuery 的链式调用设计让代码保持高度可读性,而其底层优化则确保了极致的执行效率。


标题:golang每日一库之goquery
作者:mooncakeee
地址:http://blog.dd95828.com/articles/2025/03/27/1743039425741.html
联系:scotttu@163.com