7.1 XMLの処理

XMLはデータと情報のやりとりするための形式として十分普及しています。Webサービスが日々広範囲で応用されてくるにつれ、現在XMLは日常的な開発作業において重要な役割を演じてきました。この節ではGo言語の標準パッケージにあるXML関連のパッケージをご紹介します。

この節ではXMLの規約に関する内容には触れず(もし関連した知識が必要であれば他の文献をあたってください)、どのようにGo言語で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ドキュメントは2つのサーバの情報を記述しています。サーバ名とサーバのIP情報を含んでいます。以降のGoの例ではこのXML記述に対して操作を行なっていきます。

XMLの解析

どのようにして上のXMLファイルを解析するのでしょうか?xmlパッケージのUnmarshal関数を使って目的を達成することができます。

func Unmarshal(data []byte, v interface{}) error

dataはXMLのデータストリームを受け取ります。vは出力先となる構造体です。定義はinterfaceで、XMLを任意の形式に変換することができます。ここでは主にstructの変換をご紹介します。なぜなら、structとXMLはどちらも似たようなツリー構造の特徴を持っているからです。

コード例は以下の通り:

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オブジェクトにします。上の例では以下のようなデータを出力します。

{{ 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の定義を見てみましょう:

func Unmarshal(data []byte, v interface{}) error

関数には2つの引数があることがわかります。はじめの引数は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定義のように、最後の出力は以下のようになります:

      <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パッケージで提供されるMarshalMarshalIndentという2つの関数で我々の需要を満たすことができます。この2つの関数の主な違いは2つ目の関数はプレフィックスを増加したり短縮したりする可能性があるということです。関数の定義は下の通り:

func Marshal(v interface{}) ([]byte, error)
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)

2つの関数のはじめの引数はXMLの構造体が定義する型のデータを生成するために用いられます。どちらも生成したXMLデータストリームを返します。

ここでは上のXMLをどのように出力するのか見てみましょう:

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 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であれば、各要素を出力します。value</tape>のようなものです。
  • もし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要素を出力します。
  • 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"では、3つの要素aが含むb、bが含むcが順番に出力されます。例えば以下のコードではこうなります:

      FirstName string   `xml:"name>first"`
      LastName  string   `xml:"name>last"`
    
      <name>
      <first>Asta</first>
      <last>Xie</last>
      </name>
    

ここではどのようにGo言語のxmlパッケージを使ってXMLファイルをエンコード/デコードするかご紹介しました。大切なのはXMLのすべての操作はすべてstruct tagによって実現されているという点です。より詳しい内容またはtagの定義については対応するオフィシャルドキュメントをご参照ください。

results matching ""

    No results matching ""