95
80

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GoAdvent Calendar 2014

Day 11

Goのencoding/xmlを使いこなす

Last updated at Posted at 2014-12-11

概要

こんにちは、@ono_matope です。今年はGoでencoding/xmlをよく使ったので、このパッケージの少しだけ突っ込んだ使い方を解説をしてみようと思います。

ここでは、encoding/xmlの基本的な使い方から、動的に変化するXMLの扱い方までを説明します。

基本的な使い方

まずは基本的な使い方です。encoding/xmlを使うと、Goの構造体をXML文書に変換(Marshal)、またはその逆(Unmarshal)が出来ます。例えばこんな構造体PersonをXMLにMarshal/Unmarshalが簡単に出来ます。

例1: 標準のMarshalとUnmarshal

Demo: http://play.golang.org/p/bjCr7Fpk_8

package main

import (
  "encoding/xml"
  "fmt"
)

type Name struct {
  First, Last string
}

type Person struct {
  Name   Name
  Gender string
  Age    int
}

func main() {

  // Marshalの例
  john := Person{Name{"John", "Doe"}, "Male", 20}
  buf, _ := xml.MarshalIndent(john, "", "  ")
  fmt.Println(string(buf))

  // Unmarshalの例
  xmldoc := []byte(`
  <Person>
    <Name><First>John</First><Last>Doe</Last></Name>
    <Gender>Male</Gender>
    <Age>20</Age>
  </Person>`)
  p := Person{}
  xml.Unmarshal(xmldoc, &p)
  fmt.Println(p)
}

出力

<Person>
  <Name>
    <First>John</First>
    <Last>Doe</Last>
  </Name>
  <Gender>Male</Gender>
  <Age>20</Age>
</Person>
{{John Doe} Male 20}

構造体フィールドタグ

簡単ですね。しかし、XML要素の名前が構造体メンバの名前にしまう、XML属性が出力できない、などいろいろ不便です。構造体タグを使う事で、以下のような事が出来るようになります

  • 構造体メンバとXML要素を別の名前にする
  • 構造体メンバをXML要素ではなくXML属性に対応させる
  • 構造体メンバをXMLに出力させない
  • 構造体メンバをXMLコメントに対応させる
  • などなど…

構造体フィールドタグの詳細な仕様については xml - The Go Programming Language を参照してください。

例2: さまざまな構造体フィールドタグ

Demo: http://play.golang.org/p/31lOxJLafK

package main

import "fmt"
import "encoding/xml"

func main() {
	type Person struct {
		XMLName xml.Name `xml:"person"` // Person2型のXML要素名をpersonにする
		Name    string
		Gender  string `xml:"attr"`       // XML属性にする
		Age     int    `xml:"age"`        // XMl要素名をageにする
		ID      int    `xml:"-"`          // XML要素にしない
		Note    string `xml:",omitempty"` // 空のときXML要素をつくらない
		Comment string `xml:",comment"`   // XMLコメントにする
	}

	// Marshalの例
	john := Person{Name: "John Doe", Gender: "Male", Age: 20, ID: 1, Note: "", Comment: "ノーコメント"}
	buf, _ := xml.MarshalIndent(john, "", "  ")
	fmt.Println(string(buf))

	// Unmarshalの例
	var p Person
	xml.Unmarshal(buf, &p)
	fmt.Printf("%+v", p)
}

出力

<person>
  <Name>John Doe</Name>
  <attr>Male</attr>
  <age>20</age>
  <!--ノーコメント-->
</person>
{XMLName:{Space: Local:person} Name:John Doe Gender:Male Age:20 ID:0 Note: Comment:ノーコメント}

xml.Marshaler と xml.Unmarshaler interface

しかし、構造体タグを使っても、XMLに出力したい値は全て構造体のメンバ値として保持している必要がありました。これは、例えば以下のような、変数とXMLの構造に違いがあるケースで問題になります。

  • 構造体はtime.Time型で誕生日を持っているが、XMLには生年のみ出力したい
  • xmlns属性のようなロジックと関係ない要素や属性をXMLに出力したい
  • 構造体は小文字のメンバを持っているが、XMLに出力したい(通常小文字のメンバは無視されます)

これらの要件を実装するにはどうしたらよいでしょうか?XML出力用の、BirthYearやxmlnsメンバを持つ構造体(UserInfoForXML)を定義して、元の構造体からコピーしてMarshalする?大変ですね。

そんな時は、構造体にxml.Marshaler, xml.Unmarshaler インターフェイスを実装することで、そのような構造体とXMLの変換ロジックを MarshalXML / UnmarshalXML メソッド内でスマートに記述する事が出来ます。

// xml.Marshalerインターフェイスの定義
type Marshaler interface {
        MarshalXML(e *Encoder, start StartElement) error
}

// xml.Unmarshalerインターフェイスの定義
type Unmarshaler interface {
        UnmarshalXML(d *Decoder, start StartElement) error
}

xml - The Go Programming Language

例3: 独自のMarshalXML, UnmarshalXMLメソッドを定義する

Demo: http://play.golang.org/p/8PygrpakPd

具体的には、このように実装します。この例では、以下のフォーマットのXMLと構造体を相互変換します。両者には以下の違いがあります

  • XMLにはxmlns要素がある
  • 構造体の小文字要素をXMLに出力する
  • 構造体で生年月日であるBirthTimeのうち、生年のみをXMLに出力する
type User struct {
	Name      string
	gender    string
	BirthTime time.Time
}
<User xmlns="http://www.w3.org/2001/XMLSchema-instance">
  <Name>Alice</Name>
  <Gender>Female</Gender>
  <BirthYear>2002</BirthYear>
</User>
package main

import (
	"encoding/xml"
	"errors"
	"fmt"
	"time"
)

type User struct {
	Name      string
	gender    string
	BirthTime time.Time
}

func (u User) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	// xml.Encoder に食わせる身代わり構造体の定義と初期化
	uu := struct {
		Xmlns        string `xml:"xmlns,attr"`
		Name, Gender string
		BirthYear    int
	}{
		"http://www.w3.org/2001/XMLSchema-instance",
		u.Name,
		u.gender,
		u.BirthTime.Year(),
	}
	return e.EncodeElement(uu, start)
}

func (u *User) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	// xml.Decoder に食わせる身代わり構造体の定義と初期化
	uu := struct {
		Xmlns        string `xml:"xmlns,attr"`
		Name, Gender string
		BirthYear    int
	}{}
	if err := d.DecodeElement(&uu, &start); err != nil {
		return err
	}
	// 入力値のバリデーションも可能。エラーを投げるとUnmarshal処理が停止する
	if uu.Gender != "Male" && uu.Gender != "Female" {
		return errors.New("Invalid Gender parameter")
	}
	*u = User{uu.Name, uu.Gender, time.Date(uu.BirthYear, time.January, 1, 0, 0, 0, 0, time.UTC)}
	return nil
}

func main() {
	u := User{"Alice", "Female", time.Date(2002, time.November, 10, 23, 0, 0, 0, time.UTC)}

	buf, _ := xml.MarshalIndent(u, "", " ")
	fmt.Println(string(buf))

	u2 := User{}
	xml.Unmarshal(buf, &u2)
	fmt.Printf("%+v\n", u2)
}

出力

<User xmlns="http://www.w3.org/2001/XMLSchema-instance">
 <Name>Alice</Name>
 <Gender>Female</Gender>
 <BirthYear>2002</BirthYear>
</User>
{Name:Alice gender:Female BirthTime:2002-01-01 00:00:00 +0000 UTC}

解説

xmlnsの使い方がおかしいですが、いい例題が浮かびませんでした…。MarshalXMLのレシーバは値、UnmarshalXMLのレシーバはポインタである事に注意してください。

MarshalXML では、メソッド内でXMLレイアウトに対応した匿名構造体を定義し、レシーバであるUser型の値を使って初期化したものをxml.Encoder.EncodeElement()でXMLにエンコードしています。

一方 UnmarshalXML では、メソッド内でXMLレイアウトに対応した匿名構造体を定義し、xml.Decoder.DecodeElement()でデコードした匿名構造体から、レシーバであるUser型に値を代入しています。

おまけ:動的なXMLを扱う

最後に、動的に変動するXML文書をMarshal/Unmarshalする方法を紹介します。たとえば、mapのキーバリューをXML要素として出力する事が出来ます。

例4 mapのキーバリューをXML要素にマッピングする

Demo: http://play.golang.org/p/G89t4CAAWz

この例では、文字列IDとmapのメンバを持つ構造体と、それをマッピングしたXMLの変換を扱います。IDはXML属性として扱われます。

type UserInfo struct {
	ID    string
	Extra map[string]string
}
<User id="0001">
  <Name>Charley</Name>
  <Gender>Male</Gender>
  <County>Japan</County>
  <BloodType>A</BloodType>
  <Hobby>Internet</Hobby>
</User>
package main

import (
	"encoding/xml"
	"fmt"
)

type UserInfo struct {
	ID    string
	Extra map[string]string
}

func (u UserInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	start.Name.Local = "User"
	start.Attr = []xml.Attr{{Name: xml.Name{Local: "id"}, Value: u.ID}}
	e.EncodeToken(start)
	for k, v := range u.Extra {
		e.EncodeElement(v, xml.StartElement{Name: xml.Name{Local: k}})
	}
	e.EncodeToken(start.End())
	return nil
}

func (u *UserInfo) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	u.Extra = make(map[string]string)

	// startタグの属性を走査
	for _, attr := range start.Attr {
		if attr.Name.Local == "id" {
			u.ID = attr.Value
		}
	}

	// Decoder.Token()で子Tokenの走査
	for {
		token, err := d.Token()
		if token == nil {
			break
		}
		if err != nil {
			return err
		}
		if t, ok := token.(xml.StartElement); ok {
			var data string
			if err := d.DecodeElement(&data, &t); err != nil {
				return err
			}
			u.Extra[t.Name.Local] = data
		}
	}
	return nil
}

func main() {
	u := UserInfo{
		ID: "0001",
		Extra: map[string]string{
			"Name":      "Charley",
			"Gender":    "Male",
			"County":    "Japan",
			"BloodType": "A",
			"Hobby":     "Internet",
		},
	}
	buf, _ := xml.MarshalIndent(u, "", "  ")
	fmt.Println(string(buf))

	u2 := UserInfo{}
	if err := xml.Unmarshal(buf, &u2); err != nil {
		fmt.Println(err.Error())
	}
	fmt.Printf("%#v\n", u2)
}

出力

<User id="0001">
  <Name>Charley</Name>
  <Gender>Male</Gender>
  <County>Japan</County>
  <BloodType>A</BloodType>
  <Hobby>Internet</Hobby>
</User>
main.UserInfo{ID:"0001", Extra:map[string]string{"Name":"Charley", "Gender":"Male", "County":"Japan", "BloodType":"A", "Hobby":"Internet"}}

解説

MarshalXML

マーシャル処理は、startとしてMarshalXMLメソッドにStartElementが開きタグのトークンが渡ってくるので、

  • e.EncodeToken(start) で開きタグのトークンをエンコード
  • e.EncodeElement(v, xml.StartElement{Name: xml.Name{Local: k}}) でXML要素(開きタグ-コンテンツ-閉じタグがセットになったもの)をエンコード
  • e.EncodeToken(start.End() で閉じタグのトークンをエンコード

という流れになります。

UnmarshalXML

アンマーシャル処理は、Decoder.Token()メソッドで一つずつトークンを取り出していき、開きタグ(StartElement)であればDecoder.DecodeElement(&data, &t)として値を取り出していく流れになります。(ここではフラット以上の構造を持つXMLについては扱いません)

まとめ

いかがだったでしょうか。標準のencoding/xmlでもなかなか表現力がある事が分かって頂けたかと思います。Goで変なXMLを扱う時に助けになれば幸いです。

95
80
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
95
80

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?