238 lines
10 KiB
Markdown
238 lines
10 KiB
Markdown
# 7.1 XML處理
|
||
XML作為一種資料交換和資訊傳遞的格式已經十分普及。而隨著Web服務日益廣泛的應用,現在XML在日常的開發工作中也扮演了愈發重要的角色。這一小節, 我們將就Go語言標準套件中的XML相關處理的套件進行介紹。
|
||
|
||
這個小節不會涉及XML規範相關的內容(如需瞭解相關知識請參考其他文獻),而是介紹如何用Go語言來編解碼XML檔案相關的知識。
|
||
|
||
假如你是一名運維人員,你為你所管理的所有伺服器生成了如下內容的xml的配置檔案:
|
||
```xml
|
||
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<servers version="1">
|
||
<server>
|
||
<serverName>Shanghai_VPN</serverName>
|
||
<serverIP>127.0.0.1</serverIP>
|
||
</server>
|
||
<server>
|
||
<serverName>Beijing_VPN</serverName>
|
||
<serverIP>127.0.0.2</serverIP>
|
||
</server>
|
||
</servers>
|
||
```
|
||
上面的XML文件描述了兩個伺服器的資訊,包含了伺服器名和伺服器的IP資訊,接下來的Go例子以此XML描述的資訊進行操作。
|
||
|
||
## 解析XML
|
||
如何解析如上這個XML檔案呢? 我們可以透過xml套件的`Unmarshal`函式來達到我們的目的
|
||
```Go
|
||
|
||
func Unmarshal(data []byte, v interface{}) error
|
||
```
|
||
data接收的是XML資料流,v是需要輸出的結構,定義為interface,也就是可以把XML轉換為任意的格式。我們這裡主要介紹struct的轉換,因為struct和XML都有類似樹結構的特徵。
|
||
|
||
示例程式碼如下:
|
||
```Go
|
||
|
||
package main
|
||
|
||
import (
|
||
"encoding/xml"
|
||
"fmt"
|
||
"io/ioutil"
|
||
"os"
|
||
)
|
||
|
||
type Recurlyservers struct {
|
||
XMLName xml.Name `xml:"servers"`
|
||
Version string `xml:"version,attr"`
|
||
Svs []server `xml:"server"`
|
||
Description string `xml:",innerxml"`
|
||
}
|
||
|
||
type server struct {
|
||
XMLName xml.Name `xml:"server"`
|
||
ServerName string `xml:"serverName"`
|
||
ServerIP string `xml:"serverIP"`
|
||
}
|
||
|
||
func main() {
|
||
file, err := os.Open("servers.xml") // For read access.
|
||
if err != nil {
|
||
fmt.Printf("error: %v", err)
|
||
return
|
||
}
|
||
defer file.Close()
|
||
data, err := ioutil.ReadAll(file)
|
||
if err != nil {
|
||
fmt.Printf("error: %v", err)
|
||
return
|
||
}
|
||
v := Recurlyservers{}
|
||
err = xml.Unmarshal(data, &v)
|
||
if err != nil {
|
||
fmt.Printf("error: %v", err)
|
||
return
|
||
}
|
||
|
||
fmt.Println(v)
|
||
}
|
||
|
||
```
|
||
XML本質上是一種樹形的資料格式,而我們可以定義與之匹配的go 語言的 struct型別,然後透過xml.Unmarshal來將xml中的資料解析成對應的struct物件。如上例子輸出如下資料
|
||
```xml
|
||
|
||
{{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}]
|
||
<server>
|
||
<serverName>Shanghai_VPN</serverName>
|
||
<serverIP>127.0.0.1</serverIP>
|
||
</server>
|
||
<server>
|
||
<serverName>Beijing_VPN</serverName>
|
||
<serverIP>127.0.0.2</serverIP>
|
||
</server>
|
||
}
|
||
|
||
```
|
||
上面的例子中,將xml檔案解析成對應的struct物件是透過`xml.Unmarshal`來完成的,這個過程是如何實現的?可以看到我們的struct定義後面多了一些類似於`xml:"serverName"`這樣的內容,這個是struct的一個特性,它們被稱為 struct tag,它們是用來輔助反射的。我們來看一下`Unmarshal`的定義:
|
||
```Go
|
||
|
||
func Unmarshal(data []byte, v interface{}) error
|
||
```
|
||
我們看到函式定義了兩個引數,第一個是XML資料流,第二個是儲存的對應型別,目前支援struct、slice和string,XML套件內部採用了反射來進行資料的對映,所以v裡面的欄位必須是匯出的。`Unmarshal`解析的時候XML元素和欄位怎麼對應起來的呢?這是有一個優先順序讀取流程的,首先會讀取struct tag,如果沒有,那麼就會對應欄位名。必須注意一點的是解析的時候tag、欄位名、XML元素都是大小寫敏感的,所以必須一一對應欄位。
|
||
|
||
Go語言的反射機制,可以利用這些tag資訊來將來自XML檔案中的資料反射成對應的struct物件,關於反射如何利用struct tag的更多內容請參閱reflect中的相關內容。
|
||
|
||
解析XML到struct的時候遵循如下的規則:
|
||
|
||
- 如果struct的一個欄位是string或者[]byte型別且它的tag含有`",innerxml"`,Unmarshal將會將此欄位所對應的元素內所有內嵌的原始xml累加到此欄位上,如上面例子Description定義。最後的輸出是
|
||
|
||
```xml
|
||
|
||
<server>
|
||
<serverName>Shanghai_VPN</serverName>
|
||
<serverIP>127.0.0.1</serverIP>
|
||
</server>
|
||
<server>
|
||
<serverName>Beijing_VPN</serverName>
|
||
<serverIP>127.0.0.2</serverIP>
|
||
</server>
|
||
|
||
```
|
||
|
||
- 如果struct中有一個叫做XMLName,且型別為xml.Name欄位,那麼在解析的時候就會儲存這個element的名字到該欄位,如上面例子中的servers。
|
||
- 如果某個struct欄位的tag定義中含有XML結構中element的名稱,那麼解析的時候就會把相應的element值賦值給該欄位,如上servername和serverip定義。
|
||
- 如果某個struct欄位的tag定義了中含有`",attr"`,那麼解析的時候就會將該結構所對應的element的與欄位同名的屬性的值賦值給該欄位,如上version定義。
|
||
- 如果某個struct欄位的tag定義 型如`"a>b>c"`,則解析的時候,會將xml結構a下面的b下面的c元素的值賦值給該欄位。
|
||
- 如果某個struct欄位的tag定義了`"-"`,那麼不會為該欄位解析匹配任何xml資料。
|
||
- 如果struct欄位後面的tag定義了`",any"`,如果他的子元素在不滿足其他的規則的時候就會匹配到這個欄位。
|
||
- 如果某個XML元素包含一條或者多條註釋,那麼這些註釋將被累加到第一個tag含有",comments"的欄位上,這個欄位的型別可能是[]byte或string,如果沒有這樣的欄位存在,那麼註釋將會被拋棄。
|
||
|
||
上面詳細講述瞭如何定義struct的tag。 只要設定對了tag,那麼XML解析就如上面示例般簡單,tag和XML的element是一一對應的關係,如上所示,我們還可以透過slice來表示多個同級元素。
|
||
|
||
>注意: 為了正確解析,go語言的xml套件要求struct定義中的所有欄位必須是可匯出的(即首字母大寫)
|
||
|
||
## 輸出XML
|
||
假若我們不是要解析如上所示的XML檔案,而是產生它,那麼在go語言中又該如何實現呢? xml套件中提供了`Marshal`和`MarshalIndent`兩個函式,來滿足我們的需求。這兩個函式主要的區別是第二個函式會增加字首和縮排,函式的定義如下所示:
|
||
```Go
|
||
|
||
func Marshal(v interface{}) ([]byte, error)
|
||
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
|
||
```
|
||
兩個函式第一個引數是用來產生XML的結構定義型別資料,都是返回產生的XML資料流。
|
||
|
||
下面我們來看一下如何輸出如上的XML:
|
||
```Go
|
||
|
||
package main
|
||
|
||
import (
|
||
"encoding/xml"
|
||
"fmt"
|
||
"os"
|
||
)
|
||
|
||
type Servers struct {
|
||
XMLName xml.Name `xml:"servers"`
|
||
Version string `xml:"version,attr"`
|
||
Svs []server `xml:"server"`
|
||
}
|
||
|
||
type server struct {
|
||
ServerName string `xml:"serverName"`
|
||
ServerIP string `xml:"serverIP"`
|
||
}
|
||
|
||
func main() {
|
||
v := &Servers{Version: "1"}
|
||
v.Svs = append(v.Svs, server{"Shanghai_VPN", "127.0.0.1"})
|
||
v.Svs = append(v.Svs, server{"Beijing_VPN", "127.0.0.2"})
|
||
output, err := xml.MarshalIndent(v, " ", " ")
|
||
if err != nil {
|
||
fmt.Printf("error: %v\n", err)
|
||
}
|
||
os.Stdout.Write([]byte(xml.Header))
|
||
|
||
os.Stdout.Write(output)
|
||
}
|
||
|
||
```
|
||
上面的程式碼輸出如下資訊:
|
||
```xml
|
||
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<servers version="1">
|
||
<server>
|
||
<serverName>Shanghai_VPN</serverName>
|
||
<serverIP>127.0.0.1</serverIP>
|
||
</server>
|
||
<server>
|
||
<serverName>Beijing_VPN</serverName>
|
||
<serverIP>127.0.0.2</serverIP>
|
||
</server>
|
||
</servers>
|
||
|
||
```
|
||
和我們之前定義的檔案的格式一模一樣,之所以會有`os.Stdout.Write([]byte(xml.Header))` 這句程式碼的出現,是因為`xml.MarshalIndent`或者`xml.Marshal`輸出的資訊都是不帶XML頭的,為了產生正確的xml檔案,我們使用了xml套件預定義的Header變數。
|
||
|
||
我們看到`Marshal`函式接收的引數v是interface{}型別的,即它可以接受任意型別的引數,那麼xml套件,根據什麼規則來產生相應的XML檔案呢?
|
||
|
||
- 如果v是 array或者slice,那麼輸出每一個元素,類似<type>value</type>
|
||
- 如果v是指標,那麼會Marshal指標指向的內容,如果指標為空,什麼都不輸出
|
||
- 如果v是interface,那麼就處理interface所包含的資料
|
||
- 如果v是其他資料型別,就會輸出這個資料型別所擁有的欄位資訊
|
||
|
||
產生的XML檔案中的element的名字又是根據什麼決定的呢?元素名按照如下優先順序從struct中取得:
|
||
|
||
- 如果v是struct,XMLName的tag中定義的名稱
|
||
- 型別為xml.Name的名叫XMLName的欄位的值
|
||
- 透過struct中欄位的tag來取得
|
||
- 透過struct的欄位名用來取得
|
||
- marshall的型別名稱
|
||
|
||
我們應如何設定struct 中欄位的tag資訊以控制最終xml檔案的產生呢?
|
||
|
||
- XMLName不會被輸出
|
||
- tag中含有`"-"`的欄位不會輸出
|
||
- tag中含有`"name,attr"`,會以name作為屬性名,欄位值作為值輸出為這個XML元素的屬性,如上version欄位所描述
|
||
- tag中含有`",attr"`,會以這個struct的欄位名作為屬性名輸出為XML元素的屬性,類似上一條,只是這個name預設是欄位名了。
|
||
- tag中含有`",chardata"`,輸出為xml的 character data而非element。
|
||
- tag中含有`",innerxml"`,將會被原樣輸出,而不會進行常規的編碼過程
|
||
- tag中含有`",comment"`,將被當作xml註釋來輸出,而不會進行常規的編碼過程,欄位值中不能含有"--"字串
|
||
- tag中含有`"omitempty"`,如果該欄位的值為空值那麼該欄位就不會被輸出到XML,空值包括:false、0、nil指標或nil介面,任何長度為0的array, slice, map或者string
|
||
- tag中含有`"a>b>c"`,那麼就會迴圈輸出三個元素a包含b,b包含c,例如如下程式碼就會輸出
|
||
|
||
```xml
|
||
FirstName string `xml:"name>first"`
|
||
LastName string `xml:"name>last"`
|
||
|
||
<name>
|
||
<first>Asta</first>
|
||
<last>Xie</last>
|
||
</name>
|
||
|
||
```
|
||
上面我們介紹瞭如何使用Go語言的xml套件來編/解碼XML檔案,重要的一點是對XML的所有操作都是透過struct tag來實現的,所以學會對struct tag的運用變得非常重要,在文章中我們簡要的列舉了如何定義tag。更多內容或tag定義請參看相應的官方資料。
|
||
|
||
## links
|
||
* [目錄](<preface.md>)
|
||
* 上一節: [文字處理](<07.0.md>)
|
||
* 下一節: [Json處理](<07.2.md>)
|