- 云原生
- CLI
- 应用安全
【免费下载链接】slim
Slim(toolkit): Don't change anything in your container image and minify it by up to 30x (and for compiled languages even more) making it secure too! (free and open source)
导读
本文围绕当前仓库中随附的第三方依赖 cascadia(github.com/andybalholm/cascadia,仓库中固定为 v1.3.2)展开,完整介绍如何用 CSS 选择器语法查询golang.org/x/net/html解析出的 HTML 节点树。你将掌握Parse/Query/QueryAll等核心 API、支持的完整选择器语法与组合符、底层匹配与特异性(Specificity)实现原理,并拿到一份可复制运行的定价页解析实战代码。在 Slim 项目中,该库以 vendor 方式随仓库分发,任何依赖 HTML 结构提取信息的 Go 代码都可以直接复用这套能力。
一、cascadia 是什么
cascadia 是一个纯 Go 实现的 CSS 选择器库,定位非常明确:为html包(即golang.org/x/net/html)产生的解析树提供 CSS 选择器匹配能力。它不负责解析 HTML,只负责在已经解析好的*html.Node节点树上完成"选择器 → 命中节点集合"的转换。
在当前仓库中,该库位于 vendor/github.com/andybalholm/cascadia,从 go.sum 可以看到其锁定版本为github.com/andybalholm/cascadia v1.3.2(源码由 parser.go、selector.go、pseudo_classes.go、specificity.go、serialize.go 五个文件构成)。这意味着使用本项目构建的代码可以直接import "github.com/andybalholm/cascadia"而不需要额外下载依赖。
二、核心 API 一览
cascadia 的公开接口集中定义在 selector.go 中,核心抽象是两层接口:
Matcher(selector.go#L11-L15):只要求Match(n *html.Node) bool,是最基本的匹配能力接口;Sel(selector.go#L17-L27):在Matcher之上追加Specificity()、String()与PseudoElement(),是所有解析产物的统一形态。
围绕这两个接口,库提供了以下常用函数:
| 函数 | 作用 |
|---|---|
Parse(sel string) (Sel, error) | 解析单个 CSS 选择器,不支持伪元素 |
ParseWithPseudoElement(sel string) (Sel, error) | 解析单个选择器,支持::before等伪元素 |
ParseGroup(sel string) (SelectorGroup, error) | 解析逗号分隔的选择器组 |
ParseGroupWithPseudoElements(sel string) (SelectorGroup, error) | 解析选择器组并支持伪元素 |
Compile(sel string) (Selector, error) | 兼容旧接口,返回可直接调用的Selector函数 |
MustCompile(sel string) Selector | 同Compile,出错时直接 panic |
Query(n *html.Node, m Matcher) *html.Node | 返回节点n的后代中第一个命中m的节点 |
QueryAll(n *html.Node, m Matcher) []*html.Node | 返回节点n的后代中所有命中m的节点 |
MatchAll/MatchFirst | 从n自身及其后代中查找命中节点 |
Filter(nodes []*html.Node, m Matcher) []*html.Node | 从已有节点切片中筛出命中节点 |
需要特别注意的是Query/QueryAll与MatchAll/MatchFirst的差异:前者的查询范围是n的子孙后代,不含n自身;后两者则从n自身开始判断。从源码看,Query的实现(selector.go#L177-L188)从n.FirstChild开始深度优先遍历,因此对文档根节点执行Query(doc, sel)是最常见的用法。
另外,Parse系列函数对输入有严格校验:解析结束后若字符串还有剩余字节,会返回parsing %q: %d bytes left over错误(selector.go#L38-L40),这保证了非法或截断的选择器不会被静默接受。
三、完整实战示例:解析定价页
下面这段代码是 cascadia README 自带的完整示例,展示了从Parse到QueryAll/Query的完整调用链,可直接复制运行。它的场景是:有一段包含三档订阅计划的 HTML 片段(Free / Pro / Enterprise),用 CSS 选择器把每档计划的名称、价格、人数、存储空间和详情链接全部抽取出来。
package main import ( "fmt" "log" "strings" "github.com/andybalholm/cascadia" "golang.org/x/net/html" ) var pricingHtml string = ` <div class="card mb-4 box-shadow"> <div class="card-header"> <h4 class="my-0 font-weight-normal">Free</h4> </div> <div class="card-body"> <h1 class="card-title pricing-card-title">$0/mo</h1> <ul class="list-unstyled mt-3 mb-4"> <li>10 users included</li> <li>2 GB of storage</li> <li><a href="https://example.com">See more</a></li> </ul> </div> </div> <div class="card mb-4 box-shadow"> <div class="card-header"> <h4 class="my-0 font-weight-normal">Pro</h4> </div> <div class="card-body"> <h1 class="card-title pricing-card-title">$15/mo</h1> <ul class="list-unstyled mt-3 mb-4"> <li>20 users included</li> <li>10 GB of storage</li> <li><a href="https://example.com">See more</a></li> </ul> </div> </div> <div class="card mb-4 box-shadow"> <div class="card-header"> <h4 class="my-0 font-weight-normal">Enterprise</h4> </div> <div class="card-body"> <h1 class="card-title pricing-card-title">$29/mo</h1> <ul class="list-unstyled mt-3 mb-4"> <li>30 users included</li> <li>15 GB of storage</li> <li><a>See more</a></li> </ul> </div> </div> ` func Query(n *html.Node, query string) *html.Node { sel, err := cascadia.Parse(query) if err != nil { return &html.Node{} } return cascadia.Query(n, sel) } func QueryAll(n *html.Node, query string) []*html.Node { sel, err := cascadia.Parse(query) if err != nil { return []*html.Node{} } return cascadia.QueryAll(n, sel) } func AttrOr(n *html.Node, attrName, or string) string { for _, a := range n.Attr { if a.Key == attrName { return a.Val } } return or } func main() { doc, err := html.Parse(strings.NewReader(pricingHtml)) if err != nil { log.Fatal(err) } fmt.Printf("List of pricing plans:\n\n") for i, p := range QueryAll(doc, "div.card.mb-4.box-shadow") { planName := Query(p, "h4").FirstChild.Data price := Query(p, ".pricing-card-title").FirstChild.Data usersIncluded := Query(p, "li:first-child").FirstChild.Data storage := Query(p, "li:nth-child(2)").FirstChild.Data detailsUrl := AttrOr(Query(p, "li:last-child a"), "href", "(No link available)") fmt.Printf( "Plan #%d\nName: %s\nPrice: %s\nUsers: %s\nStorage: %s\nDetails: %s\n\n", i+1, planName, price, usersIncluded, storage, detailsUrl, ) } }运行后输出如下:
List of pricing plans: Plan #1 Name: Free Price: $0/mo Users: 10 users included Storage: 2 GB of storage Details: https://example.com Plan #2 Name: Pro Price: $15/mo Users: 20 users included Storage: 10 GB of storage Details: https://example.com Plan #3 Name: Enterprise Price: $29/mo Users: 30 users included Storage: 15 GB of storage Details: (No link available)这个例子虽然短小,却覆盖了 cascadia 的几类典型用法:
- 复合类选择器
div.card.mb-4.box-shadow:要求元素同时具备标签div与三个 class,多个简单选择器在同一序列内是"与"的关系; - 后代组合符
li:last-child a:匹配li内部的a元素; - 结构伪类
:first-child、:nth-child(2)、:last-child:按兄弟节点顺序定位元素; - 属性读取:示例通过
AttrOr遍历n.Attr自行读取href,并且对缺少href的链接(Enterprise 卡的<a>See more</a>)给出默认值,这也是html.Node原始结构最直接的使用方式。
四、支持的选择器语法全景
结合 parser.go 的解析器实现,cascadia 支持的语法可以分为以下几类。
1. 基础选择器
- 类型选择器:如
div、h4,底层由tagSelector实现,要求n.Type == html.ElementNode && n.Data == tag(selector.go#L214-L217); - ID 选择器:如
#pricing,由idSelector精确匹配id属性; - 类选择器:如
.card,由classSelector按空白分隔的词列表匹配class属性,天然支持class="card mb-4"这种多类名场景。
2. 属性选择器
attrSelector(selector.go#L265-L296)支持以下全部操作符:
| 操作符 | 语义 | 实现函数 |
|---|---|---|
[attr] | 属性存在即匹配 | matchAttribute |
[attr=val] | 属性值完全相等(可忽略大小写) | matchInsensitiveValue |
[attr!=val] | 属性值不等于 val | attributeNotEqualMatch |
[attr~=val] | 属性是空白分隔词列表且包含 val | matchInclude |
[attr|=val] | 等于 val,或以 "val-" 开头 | attributeDashMatch |
[attr^=val] | 属性值以 val 开头 | attributePrefixMatch |
[attr$=val] | 属性值以 val 结尾 | attributeSuffixMatch |
[attr*=val] | 属性值包含 val | attributeSubstringMatch |
[attr#=regex] | 属性值匹配正则表达式 | attributeRegexMatch |
其中#=是 cascadia 特有的扩展操作符,它允许在属性选择器中直接书写正则(解析器通过 parseRegex 编译),使模糊匹配能力大幅增强。前缀、后缀、子串匹配在忽略大小写模式下会先做strings.ToLower再比较(selector.go#L370-L411)。
3. 组合符(Combinator)
combinedSelector的匹配逻辑(selector.go#L490-L508)实现了 CSS 标准的四种组合关系:
| 组合符 | 语义 | 底层函数 |
|---|---|---|
空格A B | 后代选择器(descendant) | descendantMatch:沿n.Parent向上回溯 |
A > B | 子选择器(child) | childMatch:要求n.Parent命中 A |
A + B | 相邻兄弟(adjacent sibling) | siblingMatch,adjacent=true,跳过文本/注释节点 |
A ~ B | 一般兄弟(general sibling) | siblingMatch,adjacent=false,向前遍历所有兄弟 |
4. 伪类(Pseudo-classes)
伪类实现在 pseudo_classes.go 中,覆盖面很广,包括:
- 结构伪类:
:first-child、:last-child、:nth-child(an+b)、:nth-last-child()、:nth-of-type()、:nth-last-of-type()、:only-child、:empty、:root; - 关系伪类:
:has(...)(相对选择器,判断是否存在匹配的子/后代节点)、:contains(text)(文本包含,以及:containsRegex变体); - 表单与状态伪类:
:input、:checked、:enabled、:disabled,其中:disabled还处理了"位于 disabled fieldset 内"等复杂 DOM 场景(pseudo_classes.go#L429-L438); - 其他:
:link(带href的<a>)、:lang(code)等。
nth-child的通用形式支持完整的an+b表达式,由 parseNth 负责解析,示例中的li:nth-child(2)就是b=2, a=0的特例。
5. 选择器组与伪元素
- 选择器组:
ParseGroup支持h1, h2, .title这类逗号分隔语法,SelectorGroup.Match只要命中其中任一选择器即返回 true(selector.go#L574-L586); - 伪元素:默认的
Parse不接受伪元素,需要调用ParseWithPseudoElement/ParseGroupWithPseudoElements。伪元素通过PseudoElement()暴露(如::before),供调用方自行处理,因为它对应的是内容渲染层面而非真实节点。
五、底层原理:匹配、特异性与序列化
1. 匹配是如何发生的
所有选择器最终都被编译为实现了Matcher接口的节点匹配器。QueryAll通过 queryInto 对节点树做先序遍历:先判断当前子节点是否命中,再递归进入其子树,命中结果依次追加到切片中。组合选择器的匹配则依赖"自底向上"的回溯:例如后代选择器A B在判断节点n时,先确认n命中B,再沿n.Parent链向上逐个检查是否存在命中A的祖先。这种设计让匹配逻辑与 CSS 标准的定义保持一一对应,容易验证正确性。
2. 特异性(Specificity)
specificity.go 定义了Specificity [3]int,分别对应 ID 数、类/属性/伪类数、类型/伪元素数。每种选择器都实现了Specificity()方法,例如tagSelector返回{0, 0, 1}(selector.go#L219-L221)、classSelector返回{0, 1, 0}、idSelector返回{1, 0, 0};复合选择器会累加各子选择器的特异性。Less方法按数组顺序逐位比较,可用于实现"同优先级时谁先定义谁生效"的 CSS 级联规则。这一特性让 cascadia 不仅能做查询,还能胜任需要排序/去重选择器的场景。
3. 序列化
serialize.go 为每种选择器实现了String()方法,可以把编译后的选择器重新输出为合法的 CSS 文本,并对标识符中的特殊字符(如空格、引号、特殊符号)做转义。这意味着你可以先Parse再String(),得到一份规范化、可安全嵌入其他 CSS 上下文的选择器表达式。
六、在当前仓库中的使用前提
cascadia 在本仓库中作为 vendored 依赖随源码分发,路径为 vendor/github.com/andybalholm/cascadia,go.sum中锁定的版本是v1.3.2。因此:
- 项目代码可以不加任何额外下载直接
import "github.com/andybalholm/cascadia",配合golang.org/x/net/html使用; - 在编写自己的 Go 程序时,只要采用"
html.Parse解析 →cascadia.Parse编译选择器 →Query/QueryAll抽取节点"这条链路,即可获得与示例一致的 CSS 查询能力; - 该库只依赖
golang.org/x/net/html的节点模型,不引入任何 CGO 或外部运行时,适合嵌入各种解析与抓取工具链。
七、小结
cascadia 用不到十个源文件,把 CSS 选择器的解析、匹配、特异性计算与序列化完整地实现了一遍,是理解"选择器引擎"这一经典主题的极佳范本。结合本文的完整示例,你可以快速上手用 Go 完成结构化的 HTML 数据抽取:先掌握Parse/Query/QueryAll三件套,再按需使用属性操作符、四种组合符与丰富的伪类,最后利用Specificity与String()处理更精细的选择器逻辑。无论是要从网页中批量提取定价信息、抓取文档结构,还是在 Slim 项目的扩展工具中做 HTML 内容分析,这套能力都能直接复用。
- 云原生
- CLI
- 应用安全
【免费下载链接】slim
Slim(toolkit): Don't change anything in your container image and minify it by up to 30x (and for compiled languages even more) making it secure too! (free and open source)
相关推荐
scan4all 依赖解析:Cascadia——基于 net/html 解析树的 Go 语言 CSS 选择器引擎
scan4all 依赖解析:Cascadia——基于 net/html 解析树的 Go 语言 CSS 选择器引擎 导读 Cascadia 是一个为 Go 语言
网络安全漏洞扫描渗透测试应用安全终极Cascadia指南:如何在Go中快速实现CSS选择器功能
终极Cascadia指南:如何在Go中快速实现CSS选择器功能 Cascadia是一个功能强大的Go语言CSS选择器库,专门用于解析和操作HTML文档。这个开源
开发工具深入理解Nokogiri CSS选择器:jQuery式查询的完整解析
深入理解Nokogiri CSS选择器:jQuery式查询的完整解析 Nokogiri是Ruby生态中最强大的XML和HTML解析工具,它提供了jQuery风格
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考