如何使用goquery进行HTML解析以及它的源码分析和实现原理

news2024/11/27 18:32:11

目录

goquery 是什么

goquery 能用来干什么

goquery quick start

玩转goquery.Find()

查找多个标签

Id 选择器

Class 选择器

属性选择器

子节点选择器

内容过滤器

goquery 源码分析

图解源码

总结


goquery 简介

goquery是一款基于Go语言的HTML解析库,它使用了类似于jQuery的语法,使得在Go语言中进行HTML解析变得更加方便。使用goquery,开发者可以在HTML文档中轻松地查询、遍历和操作文档中的各种元素和属性。

具体而言,goquery可以用来实现如下功能:

  • 在HTML文档中查找、筛选和遍历元素
  • 获取元素的属性、文本内容、HTML内容等信息
  • 对元素进行添加、修改、删除等操作
  • 在HTML文档中执行CSS选择器操作
  • 支持链式调用,可以方便地进行多个操作组合

总的来说,goquery是一款非常实用的HTML解析工具,它可以大大简化开发者在Go语言中进行HTML解析的工作。

goquery quick start

Document 是 goquery 包的核心类之一,创建一个 Document 是使用 goquery 的第一步:

type Document struct {
    *Selection
    Url      *url.URL
    rootNode *html.Node
}

func NewDocumentFromNode(root *html.Node) *Document 
func NewDocument(url string) (*Document, error)
func NewDocumentFromReader(r io.Reader) (*Document, error)
func NewDocumentFromResponse(res *http.Response) (*Document, error)

通过源码可以知道 Document 继承了 Selection(先不管 Selection 是什么),除此之外最重要的是rootNode,它是 HTML 的根节点,Url这个字段作用不大,在使用NewDocumentNewDocumentFromResponse时会对该字段赋值。

拥有Document类后,我们就可以利用从Selection类继承的Find函数来获得自己想要的数据,比如我们想拿到

func TestFind(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div>DIV2</div>
                <span>SPAN</span>
            </body>
            `
    
    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {
        log.Fatalln(err)
    }

    dom.Find("div").Each(func(i int, selection *goquery.Selection) {
        fmt.Println(selection.Text())
    })
}
------------运行结果--------------
=== RUN   TestFind
DIV1
DIV2

玩转goquery.Find()

goquery 提供了大量的函数,个人认为最重要的是Find函数,把它用好了才能快速从大量文本中筛选出我们想要的数据,下面这一章主要展示使用Find函数的各种姿势:

查找多个标签

使用,逗号找出多个标签:

func TestMultiFind(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div>DIV2</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {
        log.Fatalln(err)
    }

    dom.Find("div,span").Each(func(i int, selection *goquery.Selection) {
        fmt.Println(selection.Text())
    })
}
------------运行结果--------------
=== RUN   TestMultiFind
DIV1
DIV2
SPAN

Id 选择器

使用#代表 Id 选择器。

func TestFind_IdSelector(t *testing.T) {
    html := `<body>
                <div id="div1">DIV1</div>
                <div>DIV2</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {
        log.Fatalln(err)
    }

    dom.Find("#div1").Each(func(i int, selection *goquery.Selection) {
        fmt.Println(selection.Text())
    })
}
------------运行结果--------------
=== RUN   TestFind_IdSelector
DIV1

Class 选择器

使用.代表 Class 选择器。

func TestFind_ClassSelector(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div class="name">DIV2</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {
        log.Fatalln(err)
    }

    dom.Find(".name").Each(func(i int, selection *goquery.Selection) {
        fmt.Println(selection.Text())
    })
}
------------运行结果--------------
=== RUN   TestFind_ClassSelector
DIV2

属性选择器

使用[]代表属性选择器。

func TestFind_AttributeSelector(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div lang="zh">DIV2</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {
        log.Fatalln(err)
    }

    dom.Find("div[lang]").Each(func(i int, selection *goquery.Selection) {
        fmt.Println(selection.Text())
    })
}
------------运行结果--------------
=== RUN   TestFind_AttributeSelector
DIV2

属性选择器也支持表达式过滤,比如:

func TestFind_AttributeSelector_2(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div lang="zh">DIV2</div>
                <div lang="en">DIV3</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {
        log.Fatalln(err)
    }

    dom.Find("div[lang=zh]").Each(func(i int, selection *goquery.Selection) {
        fmt.Println(selection.Text())
    })
}
------------运行结果--------------
=== RUN   TestFind_AttributeSelector_2
DIV2
选择器说明
Find(“div[lang]”)筛选含有lang属性的div元素
Find(“div[lang=zh]”)筛选lang属性为zh的div元素
Find(“div[lang!=zh]”)筛选lang属性不等于zh的div元素
Find(“div[lang¦=zh]”)筛选lang属性为zh或者zh-开头的div元素
Find(“div[lang*=zh]”)筛选lang属性包含zh这个字符串的div元素
Find(“div[lang~=zh]”)筛选lang属性包含zh这个单词的div元素,单词以空格分开的
Find(“div[lang$=zh]”)筛选lang属性以zh结尾的div元素,区分大小写
Find(“div[lang^=zh]”)筛选lang属性以zh开头的div元素,区分大小写

当然也可以将多个属性筛选器组合,比如:Find("div[id][lang=zh]")

子节点选择器

使用>代表子节点选择器。

func TestFind_ChildrenSelector(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div>DIV2</div>
                <span>SPAN</span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {
        log.Fatalln(err)
    }

    dom.Find("body>span").Each(func(i int, selection *goquery.Selection) {
        fmt.Println(selection.Text())
    })
}
------------运行结果--------------
=== RUN   TestFind_ChildrenSelector
SPAN

此外+表示相邻,~表示共有(父节点相同即为true)

内容过滤器

过滤文本

使用:contains($text)来过滤字符串。

func TestFind_ContentFilter_Contains(t *testing.T) {
    html := `<body>
                <div>DIV1</div>
                <div>DIV2</div>
                <span>SPAN</span>
            </body>
            `
 
    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {
        log.Fatalln(err)
    }
 
    dom.Find("div:contains(V2)").Each(func(i int, selection *goquery.Selection) {
        fmt.Println(selection.Text())
    })
}
------------运行结果--------------
=== RUN   TestFind_ContentFilter_Contains
DIV2

过滤节点

func TestFind_ContentFilter_Has(t *testing.T) {
    html := `<body>
                <span>SPAN1</span>
                <span>
                    SPAN2
                    <div>DIV</div>
                </span>
            </body>
            `

    dom, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {
        log.Fatalln(err)
    }

    dom.Find("span:has(div)").Each(func(i int, selection *goquery.Selection) {
        fmt.Println(selection.Text())
    })
}
------------运行结果--------------
=== RUN   TestFind_ContentFilter_Has
SPAN2
DIV

此外,还有:first-child:first-of-type过滤器分别可以筛选出第一个子节点、第一个同类型的子节点。

相应的:last-child:last-of-type:nth-child(n):nth-of-type(n)用法类似,不做过多解释。

goquery 源码分析

Find函数 是 goquery 最核心的函数:

func (s *Selection) Find(selector string) *Selection {
    return pushStack(s, findWithMatcher(s.Nodes, compileMatcher(selector)))
}

Find函数 的功能由pushStack函数实现

func pushStack(fromSel *Selection, nodes []*html.Node) *Selection {
    result := &Selection{nodes, fromSel.document, fromSel}
    return result
}

该函数就是拿着nodes参数去创建一个新的 Selection 类,构建一个 Selection 链表。

无论是函数命名pushStack,还是 Selection 类的字段都可以证实上面的判断:

type Selection struct {
    Nodes    []*html.Node
    document *Document
    prevSel  *Selection // 上一个节点的地址
}

现在焦点来到了pushStack函数的nodes参数nodes参数是什么直接决定了我们构建了一个怎样的链表、决定了Find函数的最终返回值,这就需要我们研究下findWithMatcher函数的实现:

func findWithMatcher(nodes []*html.Node, m Matcher) []*html.Node {
    return mapNodes(nodes, func(i int, n *html.Node) (result []*html.Node) {
        for c := n.FirstChild; c != nil; c = c.NextSibling {
            if c.Type == html.ElementNode {
                result = append(result, m.MatchAll(c)...)
            }
        }
        return
    })
}

findWithMatcher函数 的功能由mapNodes函数实现:

func mapNodes(nodes []*html.Node, f func(int, *html.Node) []*html.Node) (result []*html.Node) {
    set := make(map[*html.Node]bool)
    for i, n := range nodes {
        if vals := f(i, n); len(vals) > 0 {
            result = appendWithoutDuplicates(result, vals, set)
        }
    }
    return result
}

mapNodes函数参数f的返回值[]*html.Node做去重处理,所以重点在于这个参数f func(int, *html.Node) []*html.Node的实现:

func(i int, n *html.Node) (result []*html.Node) {
    for c := n.FirstChild; c != nil; c = c.NextSibling {
        if c.Type == html.ElementNode {
            result = append(result, m.MatchAll(c)...)
        }
    }
    return
}![img.png](img.png)

函数遍历html.Node节点,并利用MatchAll函数筛选出想要的数据

type Matcher interface {
    Match(*html.Node) bool
    MatchAll(*html.Node) []*html.Node
    Filter([]*html.Node) []*html.Node
}
 
func compileMatcher(s string) Matcher {
    cs, err := cascadia.Compile(s)
    if err != nil {
        return invalidMatcher{}
    }
    return cs
}

MatchAll函数Matcher接口定义,而compileMatcher(s string)恰好通过利用cascadia库返回一个Matcher实现类,其参数s就是我们上文提到的匹配规则,比如dom.Find("div")

图解源码

使用Find函数时,goquery 做了什么:

 

总结

本文主要介绍了 goquery 最核心的Find函数的用法及其源码实现,其实除了Find函数,goquery 还提供了大量的函数帮助我们过滤数据,因为函数众多且没那么重要,本人就没有继续研究,以后有机会再深入研究下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/372834.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

聚类算法(上):8个常见的无监督聚类方法介绍和比较

无监督聚类方法的评价指标必须依赖于数据和聚类结果的内在属性&#xff0c;例如聚类的紧凑性和分离性&#xff0c;与外部知识的一致性&#xff0c;以及同一算法不同运行结果的稳定性。 本文将全面概述Scikit-Learn库中用于的聚类技术以及各种评估方法。 本文将分为2个部分&…

【Mac 教程系列】如何在 Mac 中用终端命令行方式打开 Sublime Text ?

如何在 Mac 中用终端命令行方式打开 Sublime Text ? 用 markdown 格式输出答案。 不少于1000字。细分到2级目录。 如何在 Mac 中用终端命令行方式打开 Sublime Text ? 一、首先确保已经安装 Sublime Text 前往官网https://www.sublimetext.com/下载 Sublime Text&#xff0c…

Bootstrap表单的使用

文章目录前言一、创建基础表单垂直表单&#xff08;默认&#xff09;内联表单&#xff08;水平显示&#xff09;表单控制尺寸大小给表单控件添加帮助文本禁用/只读表单无边框的控件取色器选择菜单&#xff08;默认宽度100%&#xff09;调整下拉菜单的大小表单开关滑块表单组多个…

LabVIEW网络服务安全

LabVIEW网络服务安全如何保护Web服务&#xff1f;当许多人考虑安全性时&#xff0c;他们会考虑加密、用户ID和密码。用户ID和密码用于授权&#xff08;告诉目标谁在发出请求&#xff09;。加密保护客户端和服务器之间的通信流量&#xff0c;以便未经授权的个人无法拦截和读取发…

【架构师】零基础到精通——康威定律

博客昵称&#xff1a;架构师Cool 最喜欢的座右铭&#xff1a;一以贯之的努力&#xff0c;不得懈怠的人生。 作者简介&#xff1a;一名Coder&#xff0c;软件设计师/鸿蒙高级工程师认证&#xff0c;在备战高级架构师/系统分析师&#xff0c;欢迎关注小弟&#xff01; 博主小留言…

20_FreeRTOS低功耗模式

目录 低功耗模式简介 STM32低功耗模式 Tickless模式详解 Tickless模式相关配置 实验源码 低功耗模式简介 很多应用场合对于功耗的要求很严格,比如可穿戴低功耗产品、物联网低功耗产品等。 一般MCU都有相应的低功耗模式,裸机开发时可以使用MCU的低功耗模式。 FreeRTOS也…

Linux学习(8.7)命令与文件的搜寻

目录 命令与文件的搜寻 which 文件档名的搜寻&#xff1a; whereis (寻找特定文件) locate find 以下内容转载自鸟哥的Linux私房菜 命令与文件的搜寻 which 这个命令是根据『PATH』这个环境变量所规范的路径&#xff0c;去搜寻『运行档』的档名&#xff5e; 所以&am…

计算机组成原理——运算方法续集(浮点数表示法)

浮点表示法把一个数字的有效数字和数的范围在计算机的一个存储单元中分别予以表示。这种精度分别表示的方法&#xff0c;相当于数的小数点位置随比例因子的不同而在一定范围内可以浮点&#xff0c;所以称为浮点表示法。在计算机中一个任意二进制数N可以写成N 2^e.M其中M称为浮…

Overleaf推广奖励:增加合作者的数量、解锁Dropbox同步和项目修改历史

Overleaf推广奖励 Overleaf是一个LaTeX\LaTeXLATE​X在线编译器&#xff0c;它可以让你与合作者共同在线编辑文档。但是默认的免费账号仅能邀请一个合作者。那么如何增加合作者的数量呢&#xff1f; Overleaf推出了一个奖励计划&#xff0c;你邀请其他人注册Overleaf&#xf…

使用MavenCentral发布Kotlin多平台库的远程依赖(KMM,KMP)

前言 开发者可能都会做自己的开源库,像我以前只做一些单平台的,如Android或JVM平台,这时候直接使用jitpack即可,很简单就能发布远程依赖 jitpack参考: 发布开源库的踩坑经历:jitpack.io_李小白lt的博客 而现在Kotlin可以通过expect来实现原生多平台项目(或库),这时我们开发…

【计算机三级网络技术】 第二篇 中小型系统总体规划与设计

文章目录一、基于网络的信息系统基本结构二、划分网络系统组建工程阶段三、网络需求调研与系统设计原则四、网络用户调查与网络工程需求分析1.网络用户调查2.网络节点的地理位置分布3.应用概要分析4.网络需求详细分析五、网络总体设计基本方法1.网络工程建设总体目标与设计原则…

C++---线性dp---传纸条(每日一道算法2023.2.26)

注意事项&#xff1a; 本题dp思路与 “线性dp–方格取数” 一致&#xff0c;下方思路仅证明为什么使用方格取数的思路是正确的。 题目&#xff1a; 小渊和小轩是好朋友也是同班同学&#xff0c;他们在一起总有谈不完的话题。 一次素质拓展活动中&#xff0c;班上同学安排坐成…

3.7寸按键翻页工牌

产品参数 产品型号 ESL_BWR3.7_BLE 产品尺寸 (mm) 62.51066.5 显示技术 E ink 显示区域 (mm) 47.32(H)81.12(V) 分辨率 (像素) 280480 像素尺寸(mm) 0.1690.169 150dpi 显示颜色 黑/白 视觉角度 180 工作温度 0℃ - 50℃ 电池 500mAh ( Type-C 充电…

黑盒测试用例设计方法-等价类划分法

目录 一、等价类的作用 二、等价类的分类 三、等价类的方法 四、等价类的原则 五、按照测试用例的完整性划分等价类 六、等价类步骤 七、案例 一、等价类的作用 为穷举测试设计测试点。 穷举&#xff1a;列出所有的可能情况&#xff0c;对其一一判断。 测试点&#x…

JasperReports studio相关操作

1.2 JasperReports JasperReports是一个强大、灵活的报表生成工具&#xff0c;能够展示丰富的页面内容&#xff0c;并将之转换成PDF&#xff0c;HTML&#xff0c;或者XML格式。该库完全由Java写成&#xff0c;可以用于在各种Java应用程序&#xff0c;包括J2EE&#xff0c;Web应…

【数据挖掘】1、综述:背景、数据的特征、数据挖掘的六大应用方向、有趣的案例

目录一、背景1.1 学习资料1.2 数据的特征1.3 数据挖掘的应用案例1.4 获取数据集1.5 数据挖掘的定义二、分类三、聚类四、关联分析五、回归六、可视化七、数据预处理八、有趣的案例8.1 隐私保护8.2 云计算的弹性资源8.3 并行计算九、总结一、背景 1.1 学习资料 推荐书籍如下&a…

C语言刷题(3)——“C”

各位CSDN的uu们你们好呀&#xff0c;今天小雅兰的内容还是做几道题噢&#xff0c;好好复习一下之前的知识点&#xff0c;现在&#xff0c;就让我们开始复习吧 牛客网在线编程_编程学习|练习题_数据结构|系统设计题库 倒置字符串_牛客题霸_牛客网 BC40 竞选社长 BC41 你是天才…

vitepress 就这几步操作,博客就搭好啦?

Ⅰ、什么是vitepress &#x1f48e; vitepress 使用场景 简单的说 &#xff0c;只要 会用 markdown 语法&#xff0c;就能构建自己的 「博客、笔记、使用文档」等系统 &#xff1b; ✨ vitepress 优势 优势介绍傻瓜式操作只需要配置 菜单 和 对应的 markdown 就能实现博客、笔…

OKR 与 KPI有何异同?各部门OKR实例【小bu】

OKR 与 KPI&#xff0c;如何本土化是关键 近期公司计划对去年实施的绩效考核方案进行优化&#xff0c;公司以往采用 KPI 绩效考核方式&#xff0c;产生了一些争议。一方面&#xff0c;执行期间部分部门一度忽略指标设置的真实目的&#xff0c;导致出现短视思维和行为&#xff1…

Vision Transformer学习了什么-WHAT DO VISION TRANSFORMERS LEARN? A VISUAL EXPLORATION

WHAT DO VISION TRANSFORMERS LEARN? A VISUAL EXPLORATION 文章地址 代码地址 摘要 视觉转换器( Vision Transformers&#xff0c;ViTs )正在迅速成为计算机视觉的事实上的架构&#xff0c;但我们对它们为什么工作和学习什么知之甚少。虽然现有研究对卷积神经网络的机制进…