在JSON还未像现在这么广泛使用时,XML的使用相当广泛。XML作为一种数据交换和信息传递的格式,使用还是很广泛的,现在很多开放平台接口,基本会支持XML格式。Go语言中提供了处理XML的标准库。下面我们一起来学习它。
一、encoding/xml包概述
该包实现了一个简单的XML 1.0 解析器(支持XML命名空间)
二、类型和函数
在看类型和函数之前,先看一下变量和常量
Header常量:由于Marshal生成的xml并不会生成XML标准头部,所以,定义了一个标准头常量
HTMLAutoClose变量:一些应该自动闭合的HTML标签。很明显,这是用来处理html的。这样的标签如:br、hr等
HTMLEntity变量:标准HTML字符实体的映射转换。(实体名=>实体编号)
可见,两个变量都是跟HTML相关的,之后会用到。
1、函数
func Escape(w io.Writer, s []byte)
将s中包含的特殊字符转换为实体,然后写入w中。如<转为<
func Marshal(v interface{}) ([]byte, error)
func Unmarshal(data []byte, v interface{}) error
上面两个函数的文档(注释)很长。从文档知道,Marshal是将v代表的数据转为XML格式(生成XML);而Unmarshal刚好相反,是解析XML,同时将解析的结果保存在v中。
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
这个函数和Marshal的不同是,每个XML元素会增加前缀和缩进
这三个函数的详细说明和使用在后面介绍。
2、类型(数据结构)
1)跟表示XML相关的
①type Name struct {
Space, Local string
}
Local表示本地名字,Space表示命名空间前缀(命名空间标示符),比如元素,具有本地名称polaris和命名空间前缀studygolang
该类型没有提供任何方法,主要用途一般是在XML根元素上定义一个该类型,变量名必须为XMLName,类型就为:xml.Name
②Attr(属性)、CharData(字符数据)、Comment(注释)、ProcInst(处理指令)等。这些类型都是XML标准定义的表示,一般不会用到这些。有兴趣可以查看XML标准对照着学习。
2)跟XML解析相关的
①Decoder :代表一个XML解析器,解析器假定输入是UTF-8编码
有如下字段:
Strict bool 是否允许文档错误,比如元素没关闭。在NewDecoder时,该值默认为true,这是XML的基本要求。然而,你可以自己设置为false,配合下面的字段,可以解析HTML。
AutoClose []string 当Strict是false时,在AutoClose中的元素将会自动关闭,比如:<input type=”text” />将会变为:<input type=”text” />,而不管后面有没有。当解析HTML时,AutoClose = HTMLAutoClose,如本文开始提到的,HTMLAutoClose变量定义了一些HTML中应该自动关闭的标签。
Entity map[string]string 要于HTML字符实体替换。当解析HTML时,Entity = HTMLEntity。
CharsetReader func(charset string, input io.Reader) (io.Reader, error) 这是一个函数字段,提供了一种功能,将XML中非UTF-8字符集转换为UTF-8字符集。参数charset是原字符集;input是文档来源。
还有其他非导出字段,一般不用关心,
②Encoder:输出XML数据。该类型没有提供导出的字段。
三、主要类型的方法(包括类型实例化)
在xml包中,Decoder和Encoder是两个主要的数据结构,分别解析XML和生成XML。
1、Decoder实例化和方法
func NewDecoder(r io.Reader) *Decoder
这是实例化一个Decoder,参数io.Reader,这是一个接口,它指示了要解析的XML数据源来自哪里。具体怎么使用,可以查看Unmarshal函数的实现:NewDecoder(bytes.NewBuffer(data)).Decode(v)。这里之所以使用bytes.NewBuffer,是因为bytes.Buffer实现了io.ByteReader接口。如果没有传入的参数没有实现该接口,NewDecoder内部会将其转换为:bufio.Reader。之所以这么做,是为了高效的读取(Get efficient byte at a time reader)
func (d *Decoder) Decode(v interface{}) error
该方法类似 xml.Unmarshal,只不过数据来源于decoder流,也就是实例化时的io.Reader流。其实,从上面实例化中知道,Unmarshal函数内部调用的就是Decode进行XML解析的。
func (d *Decoder) DecodeElement(v interface{}, start *StartElement) error
在Decode内部,调用的是这个方法,return d.DecodeElement(v, nil),只是start *StartElement是nil,表示从头开始解析。当只想解析某个元素之后的内容时,可以调用这个方法。
func (d *Decoder) RawToken() (Token, error)
func (d *Decoder) Token() (t Token, err error)
都是从流中获得下一个Token,RawToken不会验证开始元素和结束元素;而Token会验证(嵌套、关闭是否正确)。
func (d *Decoder) Skip() error
读Token直到找到搭配最近的开始元素为止。它可以用来跳过内部嵌套结构。找到返回nil,否则返回错误。
2、Encoder实例化和方法
func NewEncoder(w io.Writer) *Encoder
这是实例化一个Encoder,参数io.Writer是一个接口。它指示了将生成的XML输出到哪去。
func (enc *Encoder) Encode(v interface{}) error
encode v表示的数据为XML,保存到流中。Marshal的内部实现:NewEncoder(&b).Encode(v),其中,b是var b bytes.Buffer。
四、解析XML
一般的,解析XML只需要使用func Unmarshal(data []byte, v interface{}) error方法就可以。该方法接受XML数据流,和一个v,这个v是一个interface{},也就是可以“任何类型”。然而,v实际上是有要求的,它是存储解析后的XML的,要求v必须是指针,指向的类型只能是:struct、slice或string。
注意,XML解析和后面的输出XML,用的都是反射,因而struct中的字段必须可导出(首字母大写)。
1、先看一个简单的例子
package main import ( "encoding/xml" "strings" "fmt" ) func main() { var t xml.Token var err error input := `<Person><FirstName>Xu</FirstName><LastName>Xinhua</LastName></Person>` inputReader := strings.NewReader(input) // 从文件读取,如可以如下: // content, err := ioutil.ReadFile("studygolang.xml") // decoder := xml.NewDecoder(bytes.NewBuffer(content)) decoder := xml.NewDecoder(inputReader) for t, err = decoder.Token(); err == nil; t, err = decoder.Token() { switch token := t.(type) { // 处理元素开始(标签) case xml.StartElement: name := token.Name.Local fmt.Printf("Token name: %s\n", name) for _, attr := range token.Attr { attrName := attr.Name.Local attrValue := attr.Value fmt.Printf("An attribute is: %s %s\n", attrName, attrValue) } // 处理元素结束(标签) case xml.EndElement: fmt.Printf("Token of '%s' end\n", token.Name.Local) // 处理字符数据(这里就是元素的文本) case xml.CharData: content := string([]byte(token)) fmt.Printf("This is the content: %v\n", content) default: // ... } } }
程序输出:
Token name: Person
Token name: FirstName
This is the content: Xu
Token of ‘FirstName’ end
Token name: LastName
This is the content: Xinhua
Token of ‘LastName’ end
Token of ‘Person’ end
说明:
这里没有直接从文件中读取XML,而是讲XML定义在Go源码文件中,当然,从文件中读是一样的。注意,如果此处XML格式化,即多了换行和缩进,则输出结果会不一样,因为空格属于内容。
由于这里没有涉及到将XML保存到Go的某种数据类型中(比如struct),所以,xml中元素的大小写是无所谓的,不管大小写解析代码都一样处理。
这个例子可以看到解析XML后,内部结构是个什么样子,xml包中定义的各种元素代表什么。
2、解析到struct
这部分是xml的核心,一步步来。先看简单的。xml文件内容如下:
<?xml version="1.0" encoding="UTF-8"?> <Persons> <Person> <Name>polaris</Name> <Age>28</Age> <Career>无业游民</Career> <Interests> <Interest>编程</Interest> <Interest>下棋</Interest> </Interests> </Person> <Person> <Name>studygolang</Name> <Age>27</Age> <Career>码农</Career> <Interests> <Interest>编程</Interest> <Interest>下棋</Interest> </Interests> </Person> </Persons>
注意,所有标签首字母大写,没有属性,只有元素
Go解析代码如下:
package main import ( "encoding/xml" "log" "io/ioutil" ) type Result struct { Person []Person } type Person struct { Name string Age int Career string Interests Interests } type Interests struct { Interest []string } func main() { content, err := ioutil.ReadFile("studygolang.xml") if err != nil { log.Fatal(err) } var result Result err = xml.Unmarshal(content, &result) if err != nil { log.Fatal(err) } log.Println(result) }
最后输出:
{[{polaris 28 无业游民 [{[编程 下棋]}]} {studygolang 27 码农 [{[编程 下棋]}]}]}
注:这里的解析没有用到任何tag。元素首字母的大小写不能错。
简单分析一下这段代码,主要看数据结构:
Result保存最后的结果,相当于跟元素Persons,它包含了多个Person子元素,所以,咱们定义为Person []Person
而Person结构,对应Person这个元素,它包含了Name、Age、Career和Interests这些子元素,前三个只是普通的string,而Interests又包含了子元素,因而定义一个Interests类型
Interests结构对应Interests元素,包含了多个Interest元素,Interest只是一个普通的string,由于是多个,因而是一个string slice
可见,XML元素到Go中的struct只需要按XML的树状定义就可以。
我们改一下,将Name和Age改为Person的属性,Go代码不变,这个时候发现,Name和Age没有解析成功。这个时候就需要用到tag了。
我们只需要修改Person结构的定义:
type Person struct { Name string `xml:",attr"` Age int `xml:",attr"` Career string Interests Interests }
这个时候可以解析成功了。很明显,xml包会解析字段中的tag,如果tag为:`xml:”,attr”`,表示该字段是该元素的属性。注意,解析xml,要求tag必须以xml:开头,后面的必须在双引号中。这是根据反射包中StructTag类型的func (tag StructTag) Get(key string) string方法解析的,代码中固定写死了:Get(“xml”),这样来获得”,attr”这一部分。
那么”,attr”前面的逗号是啥意思?我们知道,一般xml中元素和属性等都会用小写,而我们上面的例子中,用的都是大写,如果改为小写,解析不成功(个人感觉,xml库应该支持xml元素小写时,将其首字母变为大写,当然,可能设计者更希望的是通过tag来实现xml解析)。我们可以试着将xml改为这样:
<?xml version="1.0" encoding="UTF-8"?> <Persons> <Person name="polaris" age="28"> <Career>无业游民</Career> <Interests> <Interest>编程</Interest> <Interest>下棋</Interest> </Interests> </Person> <Person Name="studygolang" Age="27"> <Career>码农</Career> <Interests> <Interest>编程</Interest> <Interest>下棋</Interest> </Interests> </Person> </Persons>
注意到,第一个Person元素中的name和age首字母小写,而第二个保持不变,而这时候我们的解析代码如下:
package main import ( "encoding/xml" "log" "io/ioutil" ) type Result struct { Person []Person } type Person struct { Name string `xml:",attr"` Age int `xml:",attr"` Career string Interests Interests } type Interests struct { Interest []string } func main() { content, err := ioutil.ReadFile("studygolang.xml") if err != nil { log.Fatal(err) } var result Result err = xml.Unmarshal(content, &result) if err != nil { log.Fatal(err) } log.Println(result) }
结果:
{[{ 0 无业游民 {[编程 下棋]}} {studygolang 27 码农 {[编程 下棋]}}]}
可见,第一个Person没解析成功,第二个成功了。这个时候可以引出为什么”,attr”前面多个逗号了。
我们将Person结构改为如下:
type Person struct { Name string `xml:"name,attr"` Age int `xml:"age,attr"` Career string Interests Interests }
这个时候执行结果为:
{[{polaris 28 无业游民 {[编程 下棋]}} { 0 码农 {[编程 下棋]}}]}
第一个Person解析成功了,但第二个Person没解析成功。你可以试着将tag中name和age首字母改为大写试试。
由此得出:tag的优先级高于Field,而且tag中也是区分大小写的。为了统一,xml中元素应该全部用小写。
注意:name、逗号和attr直接不能有空格
到现在,将xml中所有元素都改为小写,然后修改Go代码,正确解析该xml应该没问题了。
type Result struct { Person []Person `xml:"person"` } type Person struct { Name string `xml:"name,attr"` Age int `xml:"age,attr"` Career string `xml:"career"` Interests Interests `xml:"interests"` } type Interests struct { Interest []string `xml:"interest"` }
现在,是时候总结一下XML到Go中struct的转换规则了。(标准库encoding/xml文档有详细的说明)
1)如果struct的一个字段是string或者[]byte类型且它的tag含有”,innerxml”,Unmarshal会将此字段所对应的元素内所有内嵌的原始xml累加到此字段上。
2)如果struct中有一个叫做XMLName且类型为xml.Name的字段,Unmarshal会保存对应的元素的名字到该字段。比如,上面的例子,在Person结构中加上该字段XMLName xml.Name,则结果会是:{[{{ person} polaris 28 无业游民 {[编程 下棋]}} {{ person} studygolang 27 码农 {[编程 下棋]}}]}。可见,该字段是用来映射XML元素的,在生成XML时比较有用。注意,XMLName和类型xml.Name必须是这样,不能改为XmlName。
3)如果XMLName字段有tag,且tag的形式是:”name”或”namespace-URL name”,则相应的XML元素必须是这个名字(命名空间可选),否则Unmarshal会返回错误。可以通过在上面的例子中,修改Person的XMLName xml.Name `xml:”myperson”`试试,会报错:expected element typebut have
4)如果某个XML元素有一个属性,它的名字和struct中某个字段匹配(大小写都得匹配),并且该字段的tag包含”,attr”,或者元素的名字显示的被写在了tag中(”name,attr”),这时,Unmarshal会将该属性赋值给该字段。如上面的Name和Age
5)如果XML元素包含字符数据(character data),那么,字符数据会被累加到struct中第一个有tag为”,chardata”的字段。struct字段的类型可以是string或[]byte。如果没有这样的字段,字符数据会被丢弃。如上面的Interests可以再定义一个类型Interest:
type Interest struct {
Inter string `xml:”,chardata”`
}
Interests 中相应的改为:Interest []Interest
当然这个例子中这种方式有些啰嗦。
6)如果某个XML元素包含一条或者多条注释,那么这些注释将被累加到第一个含有”,comments” tag的字段上,这个字段的类型可以是[]byte或string,如果没有这样的字段存在,那么注释将会被丢弃。
7)如果某个XML元素的子元素的名字和 “a”或 “a>b>c”这种格式的tag的前缀匹配,Unmarshal会沿着XML结构向下寻找这样名字的元素,然后将最里面的元素映射到struct的字段上。以”>”开始的tag和字段后面跟上”>”是等价的。从这知道,上面例子中关于Interests的解析可以更简单,即不需要Interest结构类型
8)如果某XML元素的子元素的名字和某个struct的XMLName字段的tag匹配,且该struct的字段没有定义以上规则的tag,Unmarshal会映射该子元素到该struct的字段上。
9)如果某个XML元素的子元素的名字和一个没有任何tag的字段匹配,则Unmarshal会映射这个子元素到那个字段上。比如最开始没有使用tag的例子,使用的就是这条规则。
10)如果某个XML元素的子元素根据上面的规则都没有匹配到任何字段,然而,该struct有字段带有”,any”的tag,则Unmarshal会映射该子元素到该字段上。
11)一个非指针的匿名struct字段会被这样处理:该字段的值是外部struct的一部分
12)如果一个struct字段的tag定义为”-”,则Unmarshal不会给它赋值
这些规则,有些没有解释怎么使用,这里用标准库中的一个例子说明。(见我加的注释)
type Email struct { Where string `xml:"where,attr"` Addr string } type Address struct { City, State string } type Result struct { XMLName xml.Name `xml:"Person"` // 一般建议根元素加上此字段 Name string `xml:"FullName"` Phone string Email []Email Groups []string `xml:"Group>Value"` // 规则 7,可见字段名可以随意 Address // 规则11 } v := Result{Name: "none", Phone: "none"} data := ` <Person> <FullName>Grace R. Emlin</FullName> <Company>Example Inc.</Company> <Email where="home"> <Addr>gre@example.com</Addr> </Email> <Email where='work'> <Addr>gre@work.com</Addr> </Email> <Group> <Value>Friends</Value> <Value>Squash</Value> </Group> <City>Hanga Roa</City> <State>Easter Island</State> </Person> ` err := xml.Unmarshal([]byte(data), &v) if err != nil { fmt.Printf("error: %v", err) return } fmt.Printf("XMLName: %#v\n", v.XMLName) fmt.Printf("Name: %q\n", v.Name) fmt.Printf("Phone: %q\n", v.Phone) fmt.Printf("Email: %v\n", v.Email) fmt.Printf("Groups: %v\n", v.Groups) fmt.Printf("Address: %v\n", v.Address)
3、完整的例子
package main import ( "encoding/xml" "log" "io/ioutil" ) type Result struct { XMLName xml.Name `xml:"persons"` Persons []Person `xml:"person"` } type Person struct { Name string `xml:"name,attr"` Age int `xml:"age,attr"` Career string `xml:"career"` Interests []string `xml:"interests>interest"` } func main() { content, err := ioutil.ReadFile("studygolang.xml") if err != nil { log.Fatal(err) } var result Result err = xml.Unmarshal(content, &result) if err != nil { log.Fatal(err) } log.Println(result) log.Println(result.Persons[0].Name) }
studygolang.xml:
<?xml version="1.0" encoding="UTF-8"?> <persons> <person name="polaris" age="28"> <career>无业游民</career> <interests> <interest>编程</interest> <interest>下棋</interest> </interests> </person> <person name="studygolang" age="27"> <career>码农</career> <interests> <interest>编程</interest> <interest>下棋</interest> </interests> </person> </persons>
转自:http://blog.studygolang.com/2012/12/%E6%A0%87%E5%87%86%E5%BA%93-xml%E5%A4%84%E7%90%86%EF%BC%88%E4%B8%80%EF%BC%89