golang XML解析

在接收到微信支付成功通知的XML数据时,由于`coupon_id_n`字段的下标随数量变化,不能直接用结构体标签进行解析。本文介绍如何在Golang中通过遍历XML元素来解决这个问题。首先,解释了XML命名空间的概念,以避免标签冲突。接着,展示了处理带有命名空间的XML结构体的方法。XML解析器不会递归解析`<![CDATA[...]]>`内的内容,只会将其视为文本节点。程序运行结果证实了解析的正确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

使用微信支付的时候遇到这样一种情况:支付成功之后微信会发送一个通知过来,这个通知包含xml格式的数据,其中有一个字段是这样的:

coupon_id_ n , 代 金 券 或 立 减 优 惠 I D , n ,代金券或立减优惠ID, n,ID,n为下标,从0开始编号

也就是说我们收到的xml可能是 <coupon_id_1></coupon_id_1>也可能是<coupon_id_10></coupon_id_10>总之这个字段的名字是随着n的变化而变化的,这样的xml我们在使用golang解析的时候直接给结构体设置TAG接着使用xml.Unmarshal解析是行不通的,因为这个TAG是不确定的。这里只能挨个读取xml元素进行解析了。

<xml>
  <h:appid xmlns:h="http://www.w3school.com.cn/furniture"><![CDATA[wx2421b1c4370ec43b]]></h:appid>
  <attach name="yuanjize"><![CDATA[支付测试]]></attach>
  <bank_type>CFT</bank_type>
  <fee_type><![CDATA[CNY]]></fee_type>
  <is_subscribe><![CDATA[Y]]></is_subscribe>
  <mch_id><![CDATA[10000100]]></mch_id>
  <nonce_str><![CDATA[<hello>5d2b6c2a8db53831f7eda20af46e531c</hello>]]></nonce_str>
  <openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid>
  <out_trade_no><![CDATA[1409811653]]></out_trade_no>
  <result_code><![CDATA[SUCCESS]]></result_code>
  <return_code><![CDATA[SUCCESS]]></return_code>
  <sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign>
  <sub_mch_id><![CDATA[10000100]]></sub_mch_id>
  <time_end><![CDATA[20140903131540]]></time_end>
  <total_fee>1</total_fee>
  <coupon_fee><![CDATA[10]]></coupon_fee>
  <coupon_count><![CDATA[1]]></coupon_count>
  <coupon_type><![CDATA[CASH]]></coupon_type>
  <coupon_id><![CDATA[10000]]></coupon_id>
  <coupon_fee_0><![CDATA[100]]></coupon_fee_0>
  <trade_type><![CDATA[JSAPI]]></trade_type>
  <transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id>
</xml>

func XmlDecode(data string) map[string]string{
	decoder := xml.NewDecoder(strings.NewReader(data))
	result  := make(map[string]string)
	key := ""
	for{
		token, err := decoder.Token() //读取一个标签或者文本内容
		 if err==io.EOF{
		 	fmt.Println("parse Finish")
			 return result
		}
		if err!=nil{
			fmt.Println("parse Fail:",err)
			return result
		}
		switch tp := token.(type) {  //读取的TOKEN可以是以下三种类型:StartElement起始标签,EndElement结束标签,CharData文本内容
		case xml.StartElement:
			se := xml.StartElement(tp) //强制类型转换
			if se.Name.Local!="xml"{
				key=se.Name.Local
			}
			if len(se.Attr)!=0{ //读取标签属性
 				fmt.Println("Attrs:",se.Attr)
			}
			fmt.Println("SE.NAME.SPACE:",se.Name.Space) //读取命名空间
			fmt.Println("SE.NAME.LOCAL:",se.Name.Local) //读取标签名称
           fmt.Println()
		case xml.EndElement:
			ee := xml.EndElement(tp)
			if ee.Name.Local == "xml"{
				return result
			}
			fmt.Println("EE.NAME.SPACE:",ee.Name.Space)
			fmt.Println("EE.NAME.LOCAL:",ee.Name.Local)
		case xml.CharData: //文本数据,注意一个结束标签和另一个起始标签之间可能有空格
			cd := xml.CharData(tp)
			data := strings.TrimSpace(string(cd))
			if len(data)!=0{
			  result[key] = data
			  fmt.Println(key,",",data)
			}
		}
	}
}

上面的代码用微信的返回数据作为例子,代码比较简单,流程上就是读取一个TOKEN,判断TOKEN的类型然后把数据填到map里面。下面在说Name这个结构体之前先来料及一下XML的命名空间。

命名空间是什么?看一下下面这个例子,

<table>
    <leg>4<leg>
<table>

<table>
    <tr>
         <td>15<td>
         <td>16<td>
    <tr>
<table>

可以看到第一个table标签表示的是一个桌子,第二个table标签表示的是一个表格,如果把这个两个标签一起返回给你你就不知道两个标签各自代表的什么意思了,所以我们可以给标签加一个命名空间(也可以叫标签前缀)来区分它们:

<a:table>
    <leg>4<leg>
</a:table>

<b:table>
    <tr>
         <td>15<td>
         <td>16<td>
    <tr>
</b:table>

这样当读取到标签名字是table的时候,可以通过读取前缀来进行区分。

下面来说Name这个结构体:

type Name struct {
	Space, Local string
}

StartElementEndElement类型都有这个Name字段,它有两个字段,Space字段代表的是命名空间,Local字段是这个标签的名称。

比如<a:table> </a:table> 那么Space就是a,Local就是table。

如果命名空间是这样声明的<a:table xmls:a="www.baidu.com"> </a:table>,那么Space就是 www.baidu.comLocal还是table

这里再普及一下CDATA:xml解析器是不会解析CDATA标签中的内容。比如当解析器解析到<![CDATA[<hello>CNY</hello>]]>的时候解析出来的是一个文本类型,文本内容是<hello>CNY</hello>。也就是说把<hello>CNY</hello>看成一个单个的文本,不会去再解析hello标签。

最后贴一下上面程序的运行结果:

SE.NAME.SPACE: 
SE.NAME.LOCAL: xml

Attrs: [{{xmlns h} http://www.w3school.com.cn/furniture}]
SE.NAME.SPACE: http://www.w3school.com.cn/furniture
SE.NAME.LOCAL: appid

Attrs: [{{ name} yuanjize}]
SE.NAME.SPACE: b
SE.NAME.LOCAL: attach

SE.NAME.SPACE: 
SE.NAME.LOCAL: bank_type

SE.NAME.SPACE: 
SE.NAME.LOCAL: fee_type

SE.NAME.SPACE: 
SE.NAME.LOCAL: is_subscribe

SE.NAME.SPACE: 
SE.NAME.LOCAL: mch_id

SE.NAME.SPACE: 
SE.NAME.LOCAL: nonce_str

SE.NAME.SPACE: 
SE.NAME.LOCAL: openid

SE.NAME.SPACE: 
SE.NAME.LOCAL: out_trade_no

SE.NAME.SPACE: 
SE.NAME.LOCAL: result_code

SE.NAME.SPACE: 
SE.NAME.LOCAL: return_code

SE.NAME.SPACE: 
SE.NAME.LOCAL: sign

SE.NAME.SPACE: 
SE.NAME.LOCAL: sub_mch_id

SE.NAME.SPACE: 
SE.NAME.LOCAL: time_end

SE.NAME.SPACE: 
SE.NAME.LOCAL: total_fee

SE.NAME.SPACE: 
SE.NAME.LOCAL: coupon_fee

SE.NAME.SPACE: 
SE.NAME.LOCAL: coupon_count

SE.NAME.SPACE: 
SE.NAME.LOCAL: coupon_type

SE.NAME.SPACE: 
SE.NAME.LOCAL: coupon_id

SE.NAME.SPACE: 
SE.NAME.LOCAL: coupon_fee_0

SE.NAME.SPACE: 
SE.NAME.LOCAL: trade_type

SE.NAME.SPACE: 
SE.NAME.LOCAL: transaction_id
parse Finish

参考资料:
XML命名空间:http://www.w3school.com.cn/xml/xml_namespaces.asp
XML CDATA:http://www.w3school.com.cn/xml/xml_cdata.asp
golang XML解析:https://my.oschina.net/solate/blog/724958

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值