html.parse() 应直接传入 resp.body 以实现流式解析,避免 ioutil.readall 导致内存浪费;解析后需区分 node.type 获取文本内容,属性值从 attr 查找;dom 遍历推荐循环替代递归,特定场景优先用 html.tokenizer 提升性能。

html.Parse() 会吃掉 resp.Body,别先 ioutil.ReadAll
直接用 html.Parse(resp.Body) 是最省内存、最符合流式语义的做法。一旦你先调用 ioutil.ReadAll(resp.Body) 或 io.ReadAll(resp.Body),就把整个 HTML 加载进内存,再传给 html.Parse(strings.NewReader(...)),既多一次拷贝,又失去流式优势。
常见错误现象:大页面(>10MB)下 OOM、GC 频繁、解析延迟明显升高。
- 正确顺序:检查
resp.StatusCode→ 直接传resp.Body给html.Parse() - 错误写法:
body, _ := io.ReadAll(resp.Body); doc, _ := html.Parse(strings.NewReader(string(body))) -
html.Parse()内部会按需读取,且自动处理编码(如<meta charset="utf-8">),无需手动 decode
遍历节点时必须区分 node.Type,否则拿不到文本
HTML 解析后,html.Node 有多种类型,最常混淆的是 html.ElementNode 和 html.TextNode。链接、标题等内容的“文字”不在元素节点的 Data 字段里,而在其子节点的 TextNode.Data 中。
典型坑:写 if n.Data == "p" { fmt.Println(n.Data) },结果只打印出 p 标签名,不是段落内容。
- 要取文本内容,得递归找
html.TextNode,并跳过空白(strings.TrimSpace(n.Data) != "") - 属性值从
n.Attr列表里按attr.Key == "href"查,不是n.Data - 注释节点(
html.CommentNode)默认会被跳过,除非显式处理
DOM 遍历不用递归函数也能写得清晰
官方 golang.org/x/net/html 没提供 selector API,但靠 FirstChild/NextSibling 组合,完全可以避开深层嵌套递归,降低栈溢出风险。
比如提取所有 @#@#@#@#@#@#@#@#@#@0,重点是「找到 a 元素 → 找它的第一个非空 TextNode」,而不是无脑递归全树。
- 用 for 循环 +
c = c.FirstChild向下探,再用c = c.NextSibling横向扫,比闭包递归更易 debug - 提前 return 比 defer 更可控:找到目标就 break,别等整棵树遍历完
- 注意
n.FirstChild可能为 nil,循环条件必须判空
流式解析 tokenizer 更适合提取特定标签或统计场景
当目标只是「有没有某个 tag」「出现几次」「某属性值是否包含关键词」,用 html.NewTokenizer() 比构建完整 DOM 树快得多、省内存,尤其适合日志分析、合规扫描类任务。
它不建树,只吐 token:StartTag、EndTag、Text、Error……状态机驱动,响应极快。
- 遇到
html.StartTagToken时检查token.Data == "script",立刻跳过后续内容(用tokenizer.NextIsEOF()配合计数) - 对超长 HTML(如单页应用 SSR 输出),DOM 解析可能卡在 script 标签里;tokenizer 可设最大 token 数防 hang
- 无法随机访问父节点——这是 trade-off:你要的是速度和确定性内存占用,不是灵活性
真正难的不是怎么写遍历逻辑,而是判断该用 DOM 还是 tokenizer;以及每次拿到 html.Node 时,下意识想清楚:我现在要的是结构、内容,还是仅仅是存在性或频次。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











