如何使用goquery进行HTML解析以及它的源码分析和实现原理
目录
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 {*SelectionUrl *url.URLrootNode *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
这个字段作用不大,在使用NewDocument
和NewDocumentFromResponse
时会对该字段赋值。
拥有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.Nodedocument *DocumentprevSel *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
}
函数遍历html.Node节点
,并利用MatchAll函数
筛选出想要的数据
type Matcher interface {Match(*html.Node) boolMatchAll(*html.Node) []*html.NodeFilter([]*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 还提供了大量的函数帮助我们过滤数据,因为函数众多且没那么重要,本人就没有继续研究,以后有机会再深入研究下。
相关文章:

如何使用goquery进行HTML解析以及它的源码分析和实现原理
目录 goquery 是什么 goquery 能用来干什么 goquery quick start 玩转goquery.Find() 查找多个标签 Id 选择器 Class 选择器 属性选择器 子节点选择器 内容过滤器 goquery 源码分析 图解源码 总结 goquery 简介 goquery是一款基于Go语言的HTML解析库,…...
【Java 数组和集合 区别及使用案例】
Java中数组和集合都是用来存储一组数据的容器,但是在实际使用中,它们有一些区别和不同的使用场景。 数组 vs 集合:存储方式 数组是一个固定长度的容器,它的长度一旦被初始化之后,就无法再改变了。而集合是一个动态长…...

使用pynimate制作动态排序图
大家好,数据可视化动画使用Python包就可以完成,效果如下:想要使用Pynimate,直接import一下就行:import pynimate as nim输入数据后,Pynimate将使用函数Barplot()来创建条形数据动画。…...

Mysql 事务的隔离性(隔离级别)
Mysql 中的事务分为手动提交和自动提交,默认是自动提交,所以我们在Mysql每输入一条语句,其实就会被封装成一个事务提交给Mysql服务端。 手动提交需要先输入begin,表示要开始处理事务,然后就是常见的sql语句操作了&…...

2023年网络安全竞赛——Python渗透测试PortScan.py
端口扫描Python渗透测试:需求环境可私信博主获取 任务环境说明: 服务器场景:PYsystem0041服务器场景操作系统:未知服务器场景FTP用户名:anonymous 密码:空1. 从靶机服务器的FTP上下载PortScan.py,编辑Python程序PortScan.py,实现...

【数据结构】栈的接口实现(附图解和源码)
栈的接口实现(附图解和源码) 文章目录栈的接口实现(附图解和源码)前言一、定义结构体二、接口实现(附图解源码)1.初始化栈2.销毁栈3.入栈4.判断栈是否为空5.出栈6.获取栈顶元素7.获取栈中元素个数三、源代码…...
LC-1255. 得分最高的单词集合(回溯)
1255. 得分最高的单词集合 难度困难60 你将会得到一份单词表 words,一个字母表 letters (可能会有重复字母),以及每个字母对应的得分情况表 score。 请你帮忙计算玩家在单词拼写游戏中所能获得的「最高得分」:能够由…...

从中国文化看面试挑人标准
文章目录标准一、面相1. 1 四白眼1.2 浓眉二、讲话2.1 言多与气虚总结本文结合中国面相,是个概率性问题,对于个体无效。 标准 正直,三观正,沟通好,技术。从概率上讲: 正直且三观正的人----有恒心&#x…...
谦卑对象设计模式
谦卑设计模式介绍 “谦卑”在这里是拟人化的,指难以测试的对象清晰地认识到自己的局限性,只发挥自己的桥梁和通信作用,并不从中干预信息的传输。 谦卑对象模式‘最初的设计目的是帮助单元测试的编写者区分容易测试的行为与难以测试的行为,并将它们隔离。其设计思路…...

QML Animation动画详解
1.Animation简介 Animation类型提供了四个属性: alwaysRunToEnd:该属性接收布尔类型的参数。该属性保存动画是否运行到完成才停止。当loops属性被设置时,这个属性是最有用的,因为动画将正常播放结束,但不会重新启动。…...
C#开发的OpenRA的加载界面边框的细节
C#开发的OpenRA的加载界面边框的细节 在前面已经看到加载整个界面, 如果仔细地看,会发现加载界面的边框有一个红色的框。 这个红色的边框到底是怎么样来的呢? 其实它不是实时画上去的,而从纹理贴图里贴上去的。 也许有一些人会问,纹理贴图里的图片这么小,怎么样会有这么大…...

计算机网络笔记、面试八股(四)—— TCP连接
本章目录4. TCP连接4.1 TCP报文段的首部格式4.2 TCP连接如何保证可靠4.3 ARQ协议4.3.1 停止等待ARQ协议4.3.1.1 无差错情况4.3.1.2 出现差错情况4.3.1.3 确认丢失和确认迟到4.3.2 连续ARQ协议4.3.2.1 流水线传输4.3.2.2 累积确认4.3.2.3 滑动窗口协议4.3.3 停止等待ARQ和连续AR…...

Centos7 安装jenkins java1.8版本
1. 首先安装好jdk1.8 2. 安装jenkins 命令:(可以在根目录,创建文件夹 mkdir home 然后在此文件夹下操作 cd /home) a 清华源,获取jenkins安装包 wget https://mirrors.tuna.tsinghua.edu.cn/jenkins/redhat/jenkins-2.346-1.1.noarch.rp…...

【每日阅读】JS知识(三)
var声明提升 js是一个解释性语言类型,预解析就是在执行代码之前对代码进行通读 var关键字是,在内存中声明一个变量名 js在代码执行之前 会经历两个环节 解释代码 和执行代码 声明式函数 内存中 先声明一个变量名是函数 这个名代表的是函数 乘法表 // for…...

Vue(6)
文章目录1. 自定义指令1.1 函数式1.2 对象式1.3 自定义指令常见坑1.4 创建全局指令2. 生命周期2.1 引出生命周期2.2 分析生命周期2.3 总结3. 组件3.1 认识组件3.2 使用组件 (非单文件组件)3.3 全局组件3.4 组件的几个注意点3.5 组件的嵌套3.6 VueComponent 构造函数3.7 一个重要…...

Neo4j列表函数
使用列表 标量列表函数 size() 函数返回列表中的元素的数量 MATCH (p:Person)-[:ACTED_IN]->(m:Movie) WITH p, collect (m.title) AS MovieTitles WITH p, MovieTitles, size(MovieTitles) AS NumMovies WHERE NumMovies > 20 RETURN p.name AS Actor, NumMovies, Movie…...
55. 跳跃游戏
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。示例 1:输入:nums [2,3,1,1,4]输出:true解释:可以先跳 1 步&#…...
typedef在c语言中的作用
在 C 语言中,typedef 是一个非常有用的关键字,用于给数据类型定义一个新的名字。typedef 的作用有以下几个方面: 定义新类型名:typedef 可以定义一个新的数据类型名称,使得该类型名称可以在程序中使用。这样可以提高代…...

计算机网络体系结构及分层参考模型
文章目录一、分层设计思想的提出二、网络分层的必要性三、什么是计算机网络体系结构四、计算机网络参考模型OSI参考模型/五层参考模型/TCP/IP参考模型一、分层设计思想的提出 最早提出分层思想的是 ARPANET网。1969年11月,美国国防部开始建立一个命名为ARPANET的网络…...

LLVM程序分析与编译转换框架论文分享
LLVM 2004年论文原文 概述 本文描述了 LLVM(低级虚拟机),一种编译器框架,旨在通过在编译时、链接时、运行时,以及运行之间的空闲时间。 LLVM 以静态单一赋值 (SSA) 形式定义了一种通用的低级代码表示,具有…...

PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建
制造业采购供应链管理是企业运营的核心环节,供应链协同管理在供应链上下游企业之间建立紧密的合作关系,通过信息共享、资源整合、业务协同等方式,实现供应链的全面管理和优化,提高供应链的效率和透明度,降低供应链的成…...

第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明
AI 领域的快速发展正在催生一个新时代,智能代理(agents)不再是孤立的个体,而是能够像一个数字团队一样协作。然而,当前 AI 生态系统的碎片化阻碍了这一愿景的实现,导致了“AI 巴别塔问题”——不同代理之间…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...

九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...

安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...

基于 TAPD 进行项目管理
起因 自己写了个小工具,仓库用的Github。之前在用markdown进行需求管理,现在随着功能的增加,感觉有点难以管理了,所以用TAPD这个工具进行需求、Bug管理。 操作流程 注册 TAPD,需要提供一个企业名新建一个项目&#…...
PAN/FPN
import torch import torch.nn as nn import torch.nn.functional as F import mathclass LowResQueryHighResKVAttention(nn.Module):"""方案 1: 低分辨率特征 (Query) 查询高分辨率特征 (Key, Value).输出分辨率与低分辨率输入相同。"""def __…...

【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)
本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...

Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的----NTFS源代码分析--重要
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的 第一部分: 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…...