
本文介绍使用 go 标准库 encoding/xml 的低层 token 接口,动态解析任意 xml 元素的全部属性名与值,适用于标签类型和属性名均不固定的场景(如 html 片段解析),避免结构体反射式反序列化的局限性。
本文介绍使用 go 标准库 encoding/xml 的低层 token 接口,动态解析任意 xml 元素的全部属性名与值,适用于标签类型和属性名均不固定的场景(如 html 片段解析),避免结构体反射式反序列化的局限性。
在 Go 中,若需泛化地提取任意 XML 元素的所有属性(例如
正确做法是绕过 xml.Unmarshal / Decoder.Decode() 的结构体映射机制,改用 xml.Decoder.Token() 进行逐标记(token)的手动解析。XML 解析器会按顺序产出 xml.StartElement、xml.CharData、xml.EndElement 等标记,其中 xml.StartElement 的 Attr 字段即包含当前开始标签的全部属性切片,且完全动态、无需预定义。
以下是一个健壮、可复用的示例:
package main
import (
"bytes"
"encoding/xml"
"fmt"
)
type XMLNode struct {
Name string
Attrs []xml.Attr // 动态捕获所有属性
Children []XMLNode
}
// ParseXML 将字节流解析为树形结构,完整保留所有元素及其属性
func ParseXML(data []byte) (*XMLNode, error) {
dec := xml.NewDecoder(bytes.NewReader(data))
var root *XMLNode
var stack []*XMLNode
for {
t, err := dec.Token()
if err != nil {
if err == xml.ErrEOF {
break
}
return nil, err
}
switch se := t.(type) {
case xml.StartElement:
node := &XMLNode{
Name: se.Name.Local,
Attrs: se.Attr, // ✅ 关键:直接获取全部属性
}
if len(stack) == 0 {
root = node
} else {
stack[len(stack)-1].Children = append(stack[len(stack)-1].Children, *node)
}
stack = append(stack, node)
case xml.EndElement:
if len(stack) > 0 {
stack = stack[:len(stack)-1]
}
case xml.CharData:
// 可选:收集文本内容(此处忽略空白)
text := string(bytes.TrimSpace(se))
if text != "" && len(stack) > 0 {
// 实际项目中可扩展为 TextNode 类型
// stack[len(stack)-1].Text = text
}
}
}
return root, nil
}
func main() {
xmldata := []byte(`<div><div data-id="images/6C7161080" data-imagesize="medium" data-alignment="none"></div></div>`)
root, err := ParseXML(xmldata)
if err != nil {
panic(err)
}
// 遍历并打印所有 div 元素的属性
var walk func(*XMLNode)
walk = func(n *XMLNode) {
if n.Name == "div" {
fmt.Printf("Element: %s\n", n.Name)
fmt.Printf("Attributes (%d): ", len(n.Attrs))
for _, attr := range n.Attrs {
fmt.Printf(`%s="%s" `, attr.Name.Local, attr.Value)
}
fmt.Println()
}
for i := range n.Children {
walk(&n.Children[i])
}
}
walk(root)
}输出结果:
Element: div Attributes (0): Element: div Attributes (3): data-id="images/6C7161080" data-imagesize="medium" data-alignment="none"
✅ 关键优势:
- 属性名完全动态,无需任何结构体字段声明;
- 支持任意嵌套层级与混合内容(文本、子元素);
- 显式控制解析逻辑,避免 innerxml 导致的属性截断;
- 易于扩展(如过滤特定命名空间、跳过注释、处理 CDATA 等)。
⚠️ 注意事项:
- 若解析的是真实 HTML(非严格 XML),建议改用 golang.org/x/net/html,因其能容忍常见 HTML 错误(如自闭合标签缺失 /、大小写混用);
- xml.Attr 中的 Name.Space 字段用于处理命名空间(如 xsi:type),若需支持 XML 命名空间,应同时检查 Name.Space;
- 手动解析需自行管理栈和状态,复杂文档建议封装为可复用的 Visitor 模式或结合 xml.Decoder 的 RawToken() 提升性能。
总结:当面对属性名不可预知的 XML/HTML 场景时,放弃“结构体反射反序列化”的思维定式,转向 Token 驱动的手动解析,是 Go 中最可靠、最灵活的解决方案。










