
本文详解如何使用 golang.org/x/net/html 令牌化解析 HTML,高效提取表格()内每行()的第二个 文本,并演示如何仅保留 及其子树、剥离外部无关 HTML 内容。
本文详解如何使用 golang.org/x/net/html 令牌化解析 html,高效提取表格(<table>)内每行(<code><tr>)的第二个 <code><td> 文本,并演示如何仅保留 <code><table> 及其子树、剥离外部无关 html 内容。<p>在 Go 中进行 Web 抓取时,直接使用正则表达式解析 HTML 是<strong>不推荐且不可靠的</strong>——HTML 是嵌套结构化文本,正则无法正确处理嵌套标签、属性转义或 malformed 情况。Go 官方生态推荐使用 <code>golang.org/x/net/html 包进行基于令牌(token)的流式解析,它轻量、内存友好,且完全符合 HTML5 规范。
✅ 提取每行第二个 <td> 的文本内容<p>核心思路是:遍历 HTML 令牌流,通过状态变量追踪当前所处的 <code><tr> 和 <code><td> 层级。当遇到 <code><td> 标签时,按顺序计数(从 0 开始),仅对索引为 <code>1(即第二个)的 <td> 继续读取其内部文本节点:<pre class="brush:php;toolbar:false;">package main
import (
"fmt"
"strings"
"golang.org/x/net/html"
)
func extractSecondTD(htmlContent string) []string {
var results []string
r := strings.NewReader(htmlContent)
z := html.NewTokenizer(r)
inTR := false // 是否处于 <tr> 标签内
tdIndex := 0 // 当前行中 <td> 的序号(0-based)
for {
tt := z.Next()
switch tt {
case html.ErrorToken:
return results
case html.StartTagToken:
t := z.Token()
if t.Data == "tr" {
inTR = true
tdIndex = 0
} else if t.Data == "td" && inTR {
if tdIndex == 1 { // 第二个 <td>
// 读取下一个 token(应为 TextToken)
if z.Next() == html.TextToken {
text := strings.TrimSpace(z.Token().Data)
if text != "" {
results = append(results, text)
}
}
}
tdIndex++
}
case html.EndTagToken:
t := z.Token()
if t.Data == "tr" {
inTR = false
}
}
}
}
var sampleHTML = `
<html>
<body>
<h1>My Table</h1>
<table>
<tr><td>A1</td><td>B1</td><td>C1</td></tr>
<tr><td>A2</td><td>B2</td><td>C2</td></tr>
<tr><td>A3</td><td>B3</td><td>C3</td></tr>
</table>
</body>
</html>`
func main() {
data := extractSecondTD(sampleHTML)
fmt.Println(data) // 输出: [B1 B2 B3]
}</pre><blockquote>
<p>⚠️ 注意事项:</p>
<ul>
<li>
<code>z.Next() 必须被调用以推进解析器;未调用会导致死循环或跳过内容。
<td> 内容可能包含换行/空格,建议用 <code>strings.TrimSpace() 清理。✅ 提取 <table> 子树并生成独立 HTML 字符串<p>若需剥离 <code><table> 外所有内容(如 <code>、导航栏等),可采用“定位 + 序列化”策略:先找到 <table> 开始标签,然后递归复制其所有子节点(含开始/结束标签与文本),直到匹配闭合 <code>
。
立即学习“前端免费学习笔记(深入)”;
以下函数返回 <table>...</table> 的完整原始 HTML 字符串(含缩进与换行,保持原格式):
func extractTableOnly(htmlContent string) string {
r := strings.NewReader(htmlContent)
z := html.NewTokenizer(r)
var buf strings.Builder
depth := 0
capturing := false
for {
tt := z.Next()
switch tt {
case html.ErrorToken:
return buf.String()
case html.StartTagToken:
t := z.Token()
if t.Data == "table" && !capturing {
capturing = true
depth = 1
buf.WriteString("<" + t.Data)
for _, a := range t.Attr {
buf.WriteString(fmt.Sprintf(` %s="%s"`, a.Key, a.Val))
}
buf.WriteString(">")
continue
}
if capturing {
buf.WriteString("<" + t.Data)
for _, a := range t.Attr {
buf.WriteString(fmt.Sprintf(` %s="%s"`, a.Key, a.Val))
}
buf.WriteString(">")
if t.Data != "br" && t.Data != "img" && t.Data != "input" { // 自闭合标签除外
depth++
}
}
case html.EndTagToken:
t := z.Token()
if capturing {
buf.WriteString("</" + t.Data + ">")
if t.Data == "table" {
depth--
if depth == 0 {
return buf.String()
}
} else {
depth--
}
}
case html.TextToken:
if capturing {
text := strings.TrimSpace(z.Token().Data)
if text != "" {
buf.WriteString(html.EscapeString(text))
}
}
}
}
}? 小技巧:如需输出更规范的 HTML(如自动补全缺失闭合标签),可结合
golang.org/x/net/html的Node构建 +html.Render,但会增加内存开销;流式令牌法更适合纯裁剪场景。
总结
- ✅ 精准定位:用状态机(
inTR,tdIndex)替代 DOM 遍历,高效提取任意层级指定位置的内容; - ✅ 安全裁剪:基于令牌深度计数提取子树,避免字符串截取导致的标签断裂;
- ❌ 避免踩坑:绝不使用正则解析 HTML;注意
TextToken前必须调用z.Next();始终处理空格与转义; - ? 推荐依赖:
golang.org/x/net/html(官方维护,稳定可靠),无需引入庞大第三方库。
掌握令牌化解析,你就能在低内存、高并发场景下稳健完成网页数据抽取任务。



















