☰
html4cj访问者模式详解:用NodeVisitor与NodeFilter实现自定义节点遍历和过滤
2026/9/25 5:27:17 网站建设 项目流程

html4cj访问者模式详解:用NodeVisitor与NodeFilter实现自定义节点遍历和过滤

【免费下载链接】html4cj一个HTML格式解析库项目地址: https://gitcode.com/Cangjie-TPC/html4cj

html4cj 是一个功能完整的 HTML 解析库,可将网页 HTML 解析为 DOM 树并支持 CSS 选择器查询。本文面向新手,完整讲解 html4cj 中访问者模式的两大核心接口 NodeVisitor 与 NodeFilter:如何自定义节点遍历、如何过滤与删除节点,以及它们在库内部的实际应用,帮你快速上手这套树遍历机制。

一、为什么需要访问者模式?🌳

html4cj 解析 HTML 后,会得到一棵 DOM 树:文档、元素、文本、注释都是节点(Node)的实例。而很多需求都绕不开"把树上的节点逐个处理一遍":

  • 统计页面中所有img标签
  • 清理 HTML 中的script、style标签
  • 把整个文档序列化回 HTML 字符串(outerHtml)
  • CSS 选择器查询doc.select("div p")

如果每次都写一套递归逻辑,代码就会大量重复。html4cj 的解决方案是访问者模式(Visitor Pattern):由库负责"怎么走树",你只需要提供"走到每个节点时做什么"的回调。整个流程如图所示——解析器先构建 DOM 树,之后的所有遍历都发生在这棵树上:

这套机制的核心由三个文件组成:

  • 访问者接口:node_visitor.cj
  • 过滤器接口:node_filter.cj
  • 遍历引擎:node_traversor.cj

二、NodeVisitor:两行回调实现自定义节点遍历

NodeVisitor是一个极简接口,只有两个回调方法:

方法触发时机说明
head(node, depth)首次访问节点时必须实现,参数为当前节点和相对根节点的深度
tail(node, depth)访问完所有子孙节点后有默认空实现,只关心head时可不重写

使用方式非常直接:任何节点(Node、Element、Document)和元素列表(Elements)都内置了traverse()方法,例如 node.cj 和 elements.cj。一个典型的"统计所有元素数量"的访问者大致如下:

class CountVisitor <: NodeVisitor { public override func head(node: Node, depth: Int64): Unit { if (node is Element) { count++ } } } // 使用:doc.traverse(CountVisitor())

遍历引擎 NodeTraversor 内部采用深度优先策略:先向下钻进第一个子节点,遇到叶子后再沿兄弟节点横移,没有兄弟时逐级回退并触发tail回调。整个过程你无需关心,只需专注回调逻辑。

💡 小细节:traverse是安全的动态遍历——即使你在head回调中删除或替换了当前节点,引擎也能正确继续(它会在访问前预先记录父节点和兄弟位置),所以访问者里做增删改是安全的。

三、NodeFilter:用5种过滤决策控制遍历走向 ✂️

如果只"看"不够,还想改变遍历路径或直接修改树,就轮到NodeFilter上场了。它与NodeVisitor结构相同(同样是head/tail两个回调),区别在于回调返回的不是 Unit,而是一个FilterResult决策值,共 5 种,定义见 node_filter.cj:

决策值效果典型用途
CONTINUE继续正常遍历(默认)不干预
SKIP_CHILDREN跳过该节点的子树,但仍会调用tail只处理某层标签本身
SKIP_ENTIRELY跳过子树且不再调用tail快速略过无关区域
REMOVE删除该节点及其所有子节点清理 script/style 标签
STOP立即终止整个遍历找到第一个目标后停止

使用方式同样是内置的filter()方法,见 node.cj。一个"删除所有 script 标签"的过滤器:

class ScriptCleaner <: NodeFilter { public override func head(node: Node, depth: Int64): FilterResult { if (node is Element && (node as Element)().normalName() == "script") { return FilterResult.REMOVE } return FilterResult.CONTINUE } } // 使用:doc.filter(ScriptCleaner())

引擎对REMOVE的处理非常讲究:会先找到父节点/兄弟节点再执行删除(见 node_traversor.cj),避免删除后游标悬空导致遍历错乱——这也是你使用REMOVE时不需要任何额外处理的原因。

四、对比速查:遍历 vs 过滤怎么选?⚖️

维度NodeVisitor(traverse)NodeFilter(filter)
回调返回值无(Unit)FilterResult决策
能否修改树✅ 支持删除/替换,引擎自动容错✅ 支持REMOVE剪枝
能否改变遍历路径❌ 固定深度优先走完全树✅ 可跳过、可提前STOP
典型场景收集信息、生成输出清洗 HTML、查找首个匹配
入口方法node.traverse(visitor)node.filter(nodeFilter)

一句话总结:只读分析用 Visitor,需要修剪或提前终止用 Filter。

五、实战彩蛋:html4cj 内部就是这么用的 🔍

这套模式不是摆设,库内部的核心功能全部基于它构建,值得对照源码学习:

  1. HTML 序列化:outerHtml()使用的OuterHtmlVisitor,把每个节点的 HTML 片段按顺序追加到字符串缓冲中,见 node.cj
  2. CSS 选择器查询:doc.select("a")背后的 Collector 就是一个NodeVisitor——每个head回调里用选择器求值器测试节点,匹配则收入结果集
  3. selectFirst 提前终止:FirstFinder是一个NodeFilter,命中第一个匹配元素立即返回STOP,避免扫完全树,见 collector.cj
  4. 文本提取:Element.text()通过TextNodeVisitor遍历收集文本节点,见 element.cj

六、新手常见坑点提醒 ⚠️

  • 不要手动递归替代 traverse:手写递归很难处理"遍历中节点被删除/替换"的边界情况,NodeTraversor已替你处理
  • SKIP_CHILDREN与SKIP_ENTIRELY的区别在于是否仍触发tail,若你的tail里有清理逻辑要选对
  • REMOVE是连子节点一起删,只想删标签外壳保留内容时应改用unwrap()这类 API
  • STOP只终止当前过滤,对Elements批量过滤时,会在命中STOP后停止处理后续元素,见 node_traversor.cj

结语

html4cj 通过NodeVisitor+NodeFilter这两个小接口,把"树怎么走"和"节点做什么"彻底解耦:几行回调即可实现自定义遍历、节点清洗、提前终止查询等操作,这正是访问者模式在解析库中的教科书式落地。想进一步熟悉全部 API,可查阅官方接口文档 doc/feature_api.md,配合 node_traversor.cj 的源码,基本就能玩转 html4cj 的树遍历体系了。

【免费下载链接】html4cj一个HTML格式解析库项目地址: https://gitcode.com/Cangjie-TPC/html4cj

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询