Golang中如何获取`*html.Node`解析前的原始字符串?

Golang中如何获取*html.Node解析前的原始字符串? 你好

如何获取解析 *html.Node 的原始字符串?

使用 html.Render 渲染节点对我来说不够,因为我需要逐字符的原始字符串。

我倾向于不分叉和修改 golang.org/x/net/html 的内部结构,但我怀疑这是唯一的方法 :-(。

有什么想法吗?

谢谢

3 回复

我认为你无法直接使用那个包来实现这个功能。你为什么需要逐个字符地获取原始字符串呢?

更多关于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.NodeData 字段和其 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 结合自定义解析器)。

回到顶部