7.14. 示例: 基于标记的XML解码

第4.5章节展示了如何使用encoding/json包中的Marshal和Unmarshal函数来将JSON文档转换成Go语言的数据结构。encoding/xml包提供了一个相似的API。当我们想构造一个文档树的表示时使用encoding/xml包会很方便,但是对于很多程序并不是必须的。encoding/xml包也提供了一个更低层的基于标记的API用于XML解码。在基于标记的样式中,解析器消费输入并产生一个标记流;四个主要的标记类型-StartElement,EndElement,CharData,和Comment-每一个都是encoding/xml包中的具体类型。每一个对(*xml.Decoder).Token的调用都返回一个标记。

这里显示的是和这个API相关的部分:

encoding/xml

  1. package xml
  2. type Name struct {
  3. Local string // e.g., "Title" or "id"
  4. }
  5. type Attr struct { // e.g., name="value"
  6. Name Name
  7. Value string
  8. }
  9. // A Token includes StartElement, EndElement, CharData,
  10. // and Comment, plus a few esoteric types (not shown).
  11. type Token interface{}
  12. type StartElement struct { // e.g., <name>
  13. Name Name
  14. Attr []Attr
  15. }
  16. type EndElement struct { Name Name } // e.g., </name>
  17. type CharData []byte // e.g., <p>CharData</p>
  18. type Comment []byte // e.g., <!-- Comment -->
  19. type Decoder struct{ /* ... */ }
  20. func NewDecoder(io.Reader) *Decoder
  21. func (*Decoder) Token() (Token, error) // returns next Token in sequence

这个没有方法的Token接口也是一个可识别联合的例子。传统的接口如io.Reader的目的是隐藏满足它的具体类型的细节,这样就可以创造出新的实现:在这个实现中每个具体类型都被统一地对待。相反,满足可识别联合的具体类型的集合被设计为确定和暴露,而不是隐藏。可识别联合的类型几乎没有方法,操作它们的函数使用一个类型分支的case集合来进行表述,这个case集合中每一个case都有不同的逻辑。

下面的xmlselect程序获取和打印在一个XML文档树中确定的元素下找到的文本。使用上面的API,它可以在输入上一次完成它的工作而从来不要实例化这个文档树。

gopl.io/ch7/xmlselect

  1. // Xmlselect prints the text of selected elements of an XML document.
  2. package main
  3. import (
  4. "encoding/xml"
  5. "fmt"
  6. "io"
  7. "os"
  8. "strings"
  9. )
  10. func main() {
  11. dec := xml.NewDecoder(os.Stdin)
  12. var stack []string // stack of element names
  13. for {
  14. tok, err := dec.Token()
  15. if err == io.EOF {
  16. break
  17. } else if err != nil {
  18. fmt.Fprintf(os.Stderr, "xmlselect: %v\n", err)
  19. os.Exit(1)
  20. }
  21. switch tok := tok.(type) {
  22. case xml.StartElement:
  23. stack = append(stack, tok.Name.Local) // push
  24. case xml.EndElement:
  25. stack = stack[:len(stack)-1] // pop
  26. case xml.CharData:
  27. if containsAll(stack, os.Args[1:]) {
  28. fmt.Printf("%s: %s\n", strings.Join(stack, " "), tok)
  29. }
  30. }
  31. }
  32. }
  33. // containsAll reports whether x contains the elements of y, in order.
  34. func containsAll(x, y []string) bool {
  35. for len(y) <= len(x) {
  36. if len(y) == 0 {
  37. return true
  38. }
  39. if x[0] == y[0] {
  40. y = y[1:]
  41. }
  42. x = x[1:]
  43. }
  44. return false
  45. }

main函数中的循环每遇到一个StartElement时,它把这个元素的名称压到一个栈里,并且每次遇到EndElement时,它将名称从这个栈中推出。这个API保证了StartElement和EndElement的序列可以被完全的匹配,甚至在一个糟糕的文档格式中。注释会被忽略。当xmlselect遇到一个CharData时,只有当栈中有序地包含所有通过命令行参数传入的元素名称时,它才会输出相应的文本。

下面的命令打印出任意出现在两层div元素下的h2元素的文本。它的输入是XML的说明文档,并且它自己就是XML文档格式的。

  1. $ go build gopl.io/ch1/fetch
  2. $ ./fetch http://www.w3.org/TR/2006/REC-xml11-20060816 |
  3. ./xmlselect div div h2
  4. html body div div h2: 1 Introduction
  5. html body div div h2: 2 Documents
  6. html body div div h2: 3 Logical Structures
  7. html body div div h2: 4 Physical Structures
  8. html body div div h2: 5 Conformance
  9. html body div div h2: 6 Notation
  10. html body div div h2: A References
  11. html body div div h2: B Definitions for Character Normalization
  12. ...

练习 7.17: 扩展xmlselect程序以便让元素不仅可以通过名称选择,也可以通过它们CSS风格的属性进行选择。例如一个像这样

  1. <div id="page" class="wide">

的元素可以通过匹配id或者class,同时还有它的名称来进行选择。

练习 7.18: 使用基于标记的解码API,编写一个可以读取任意XML文档并构造这个文档所代表的通用节点树的程序。节点有两种类型:CharData节点表示文本字符串,和 Element节点表示被命名的元素和它们的属性。每一个元素节点有一个子节点的切片。

你可能发现下面的定义会对你有帮助。

  1. import "encoding/xml"
  2. type Node interface{} // CharData or *Element
  3. type CharData string
  4. type Element struct {
  5. Type xml.Name
  6. Attr []xml.Attr
  7. Children []Node
  8. }