转:标准库—XML处理


在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中。如<转为&lt;

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

Archives