如何使用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) 形式定义了一种通用的低级代码表示,具有…...

《程序员思维修炼》速读笔记
文章目录书籍信息概览绪论从新手到专家的历程认识大脑利用右脑调试大脑主动学习积累经验控制注意力超越专家图解书籍信息 书名:《程序员思维修炼(修订版)》 作者:[美] Andy Hunt 概览 绪论 再提“实用”关注情境所有人都关注这…...

【Hello Linux】进程概念
作者:小萌新 专栏:Linux 作者简介:大二学生 希望能和大家一起进步! 本篇博客简介:简单介绍下进程的概念 进程基本概念PCB 程序控制块task_struct是什么task_struct里面有什么查看进程通过系统目录查看进程通过ps指令查…...

Bunifu.UI.WinForms 6.0.2 Crack
Bunifu.UI.WinForms为 WinForms创建令人惊叹的UI Bunifu.UI.WinForms我们为您提供了现代化的快速用户界面控件。用于 WinForms C# 和 VB.NET 应用程序开发的完美 UI 工具 简单 Bunifu.UI.WinForms没有臃肿的特征。正是您构建令人惊叹的 WinForms 应用程序所需要的。只需拖放然…...

学习 Python 之 Pygame 开发魂斗罗(五)
学习 Python 之 Pygame 开发魂斗罗(五)继续编写魂斗罗1. 加载地图2. 修改角色尺寸和地面高度继续编写魂斗罗 在上次的博客学习 Python 之 Pygame 开发魂斗罗(四)中,我们完成了角色的移动和跳跃还有射击,由…...

LeetCode 104. 二叉树的最大深度
LeetCode 104. 二叉树的最大深度 难度:easy\color{Green}{easy}easy 题目描述 给定一个二叉树,找出其最大深度。 二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 说明: 叶子节点是指没有子节点的节点。 示例: 给定二叉树 [3…...

pandas 中如何按行或列的值对数据排序?
在处理表格型数据时,常会用到排序,比如,按某一行或列的值对表格排序,要怎么做呢? 这就要用到 pandas 中的 sort_values() 函数。 一、 按列的值对数据排序 先来看最常见的情况。 1.按某一列的值对数据排序 以下面…...

「牛客网C」初学者入门训练BC139,BC158
🐶博主主页:ᰔᩚ. 一怀明月ꦿ ❤️🔥专栏系列:线性代数,C初学者入门训练 🔥座右铭:“不要等到什么都没有了,才下定决心去做” 🚀🚀🚀大家觉不错…...

【深度学习】线性回归、逻辑回归、二分类,多分类等基础知识总结
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录前言1. 线性回归2、逻辑回归3. 单层神经元的缺陷&多层感知机softmax 多分类最后再来一个 二分类的例子前言 入行深度学习快2年了,是时间好好总结下基础知识了.现…...

【MySQL】调控 字符集
一、 MySQL 启动选项 & 系统变量 启动选项 是在程序启动时我们程序员传递的一些参数,而 系统变量 是影响服务器程序运行行为的变量 1.1 启动项 MySQL 客户端设置项包括: 允许连入的客户端数量 、 客户端与服务器的通信方式 、 表的默认存储引擎 、…...

FME+YOLOV7写DNF自动刷图脚本
目录 前言 一、难点分析 二、实现流程 1.DNF窗口位置获取 2.获取训练数据 3.数据标注 4.数据格式转换 5.数据训练 5.刷图逻辑编写 前言 这是一篇不务正业的研究,首先说明,这不是外挂!这不是外挂!这不是外挂!这只是用a…...