GoLang解析XML,非Unmarshal方法

看网上好多都是用的Unmarshal函数,但是对于有些不知道节点数量的情况,Unmarshal函数还是无法很好完成。所以自己写了个方法,在这里记下来,免得忘了。

package loader

import (
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"strings"
)

// Node XML的节点结构体
type Node struct {
	name       string            // 标签名称
	attr       map[string]string // 标签属性Map
	value      string            // 节点的值
	level      int               // 节点所在层级
	parentNode *Node             // 父节点
	childNodes []*Node           // 子节点切片
}

// PrintNode 在控制台输出节点左右信息
func (node *Node) PrintNode() {

	var attrbutes string = ""

	if len(node.attr) > 0 {

		attrbutes = "ATTR: "

		for key, value := range node.attr {

			attrbutes = attrbutes + key + "-" + value + "; "
		}
	}

	if node.parentNode == nil {

		fmt.Println(node.name, node.value, attrbutes)

	} else {

		fmt.Println(node.parentNode.name, node.name, node.value, attrbutes)
	}

	childNodes := node.childNodes

	if len(childNodes) != 0 {

		for _, childNode := range childNodes {

			childNode.PrintNode()
		}
	}
}

// stack 用来压入XML的节点。
type stack struct {
	MaxTop int     // 栈顶最大值
	Top    int     // 栈顶标识
	arr    []*Node // 数组
}

func (s *stack) push(node *Node) error {

	if s.Top == s.MaxTop-1 {
		return errors.New("push(), stack full")
	}

	s.Top++
	s.arr[s.Top] = node

	return nil
}

func (s *stack) pop() (node *Node, err error) {

	if s.Top == -1 {
		return nil, errors.New("pop(), stack empty")
	}

	node = s.arr[s.Top]
	s.Top--

	return node, nil
}

func (s *stack) topValue() *Node {

	return s.arr[s.Top]
}

func initStack(MaxTop, Top int) *stack {

	// 创建栈对象,最多支持16层嵌套
	return &stack{
		MaxTop: 16,
		Top:    -1,
		arr:    make([]*Node, 16),
	}
}

// LoadXML 通过文件路径加载xml,并返回解析的节点链
func LoadXML(xmlDec *xml.Decoder) (root *Node, err error) {

	// 初始化栈
	s := initStack(10, -1)

	// 创建节点链
	root = new(Node)
	var node *Node

	var startElementLoaded bool = false

	for {

		// 获取xml的token。
		// ** 这个地方比较坑,标签和标签之间如果有换行,换行符和下一行开始标签开始之前的空格会识别为xml.CharData类型。
		token, err := xmlDec.Token()

		if err == io.EOF {
			err = nil
			break
		}

		if err != nil {
			return nil, errors.New("loader.LoadXML, xmlDec.Token() error")
		}

		// 判断节点类型
		switch t := token.(type) {

		// 开始标签,主要进行标签名压栈和添加父子节点链接
		case xml.StartElement:

			name := t.Name.Local
			var attr map[string]string

			if len(t.Attr) != 0 {

				attr = make(map[string]string)

				for _, xmlAttr := range t.Attr {
					attr[xmlAttr.Name.Local] = xmlAttr.Value
				}
			}

			if s.Top == -1 {

				root.name = name
				root.attr = attr
				root.level = s.Top
				root.childNodes = make([]*Node, 0)

				s.push(root)

			} else {

				node = &Node{
					name:       name,
					attr:       attr,
					level:      s.Top,
					childNodes: make([]*Node, 0),
				}

				previousNode := s.topValue()

				node.parentNode = previousNode
				previousNode.childNodes = append(previousNode.childNodes, node)

				// 只有在非根节点的开始元素载入时才会设置为true
				startElementLoaded = true
				s.push(node)
			}

		// 将值存入节点链中最后一个节点
		case xml.CharData:

			// 根节点后面的换行符会被识别为xml.CharData类型,currentNode还没有被实例化,
			// 此时向currentNode写入值会产生panic,所以在此处要做一个非nil的判断
			if node != nil {

				// 判断是否已经载入了开始元素
				if startElementLoaded {

					// 处理开始元素后面的换行符和第二行的空格
					value := string(t)
					value = strings.Replace(value, "\n", "", -1)
					node.value = value
				}
			}

		// 结束标签,主要将当前标签名弹出栈
		case xml.EndElement:

			startElementLoaded = false
			s.pop()

		// Comment,ProcInst和Directive类型的内容不做处理
		case xml.Comment:
		case xml.ProcInst:
		case xml.Directive:
		default:
			panic(errors.New("parse failed"))
		}
	}

	return
}

这样在获得节点链之后,就可以按自己的需求进行处理了。

PS:之前版本写得太烂,重新修改了一下。单元测试和基准测试的代码在https://github.com/luo2pei4/tools/tree/master下面。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值