Golang中如何获取`*html.Node`解析前的原始字符串?
Golang中如何获取*html.Node解析前的原始字符串?
你好
如何获取解析 *html.Node 的原始字符串?
使用 html.Render 渲染节点对我来说不够,因为我需要逐字符的原始字符串。
我倾向于不分叉和修改 golang.org/x/net/html 的内部结构,但我怀疑这是唯一的方法 :-(。
有什么想法吗?
谢谢
我认为你无法直接使用那个包来实现这个功能。你为什么需要逐个字符地获取原始字符串呢?
更多关于Golang中如何获取`*html.Node`解析前的原始字符串?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
我正在编写一个程序,用于爬取网站并查找指向不可用资源的引用(链接、图片、样式表导入等),然后生成报告。在报告中,如果能包含原始字符串会很有用。例如,假设它找到一个指向不可用资源的链接 <a href="https://gøøglæ.com">google.com</a>,我希望报告能这样显示:
发现一个链接
<a href="https://gøøglæ.com">google.com</a>,GET https://gøøglæ.com:出现错误
如果我仅仅对节点进行 html.Render 并将其包含在报告中,用户可能会复制 HTML <a href="https://gøøglæ.com">google.com</a>,然后在他们的 HTML 文件中使用 Ctrl-F 搜索它,并困惑为什么得到 0 个结果。这就是为什么我需要逐字符的原始字符串。
目前,我只是在报告中写了一条警告信息,内容类似于“报告中的 HTML 可能与网页中的 HTML 在字符级别上不完全相同”,但我对这个解决方案并不满意。
你可以通过 *html.Node 的 Data 字段和其 Attr 属性来重建原始标签,但这无法完全保留原始格式(如空格、注释等)。如果需要逐字符的原始字符串,确实需要访问解析器的原始输入。以下是两种方法:
方法1:通过解析器的原始数据(需修改解析器行为)
在解析时记录每个节点的原始字符串范围。这需要自定义解析器,但可以不修改 golang.org/x/net/html 的源码,而是包装其解析过程:
package main
import (
"fmt"
"strings"
"golang.org/x/net/html"
)
type RawNode struct {
*html.Node
Start, End int // 在原始HTML中的字节偏移
}
func ParseWithRaw(htmlStr string) ([]RawNode, error) {
r := strings.NewReader(htmlStr)
tokenizer := html.NewTokenizer(r)
var nodes []RawNode
var stack []RawNode
for {
tt := tokenizer.Next()
rawStart := len(htmlStr) - r.Len() - len(tokenizer.Raw())
rawEnd := len(htmlStr) - r.Len()
switch tt {
case html.ErrorToken:
return nodes, nil
case html.StartTagToken, html.SelfClosingTagToken:
tn, _ := tokenizer.TagName()
n := &html.Node{
Type: html.ElementNode,
Data: string(tn),
}
rawNode := RawNode{Node: n, Start: rawStart, End: rawEnd}
nodes = append(nodes, rawNode)
stack = append(stack, rawNode)
case html.EndTagToken:
if len(stack) > 0 {
stack = stack[:len(stack)-1]
}
case html.TextToken:
if len(stack) > 0 {
n := &html.Node{
Type: html.TextNode,
Data: string(tokenizer.Text()),
}
nodes = append(nodes, RawNode{Node: n, Start: rawStart, End: rawEnd})
}
}
}
}
func main() {
htmlStr := `<div class="test">Hello</div>`
nodes, _ := ParseWithRaw(htmlStr)
for _, n := range nodes {
fmt.Printf("Node: %s, Raw: %s\n", n.Data, htmlStr[n.Start:n.End])
}
}
方法2:通过节点位置信息直接截取原始字符串
如果你知道每个节点在原始HTML中的位置,可以直接截取。这需要解析时记录位置信息:
package main
import (
"fmt"
"golang.org/x/net/html"
"strings"
)
func ParseAndMap(htmlStr string) (map[*html.Node]string, error) {
doc, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
return nil, err
}
rawMap := make(map[*html.Node]string)
var f func(*html.Node, int) int
f = func(n *html.Node, pos int) int {
start := pos
if n.Type == html.ElementNode {
pos += len("<" + n.Data)
for _, a := range n.Attr {
pos += len(" " + a.Key + `="` + a.Val + `"`)
}
if n.FirstChild == nil {
pos += len("/>")
rawMap[n] = htmlStr[start:pos]
return pos
} else {
pos += len(">")
}
} else if n.Type == html.TextNode {
rawMap[n] = htmlStr[pos:pos+len(n.Data)]
return pos + len(n.Data)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
pos = f(c, pos)
}
if n.Type == html.ElementNode && n.FirstChild != nil {
rawMap[n] = htmlStr[start:pos] + "</" + n.Data + ">"
pos += len("</" + n.Data + ">")
}
return pos
}
f(doc, 0)
return rawMap, nil
}
func main() {
htmlStr := `<p>Example <b>text</b></p>`
rawMap, _ := ParseAndMap(htmlStr)
for node, raw := range rawMap {
fmt.Printf("Node Type: %v, Raw: %q\n", node.Type, raw)
}
}
注意:这些方法无法完美处理所有HTML边缘情况(如注释、脚本内容等)。如果需要完全精确的原始字符串,建议使用支持位置信息的HTML解析器(如 github.com/PuerkitoBio/goquery 结合自定义解析器)。

