😊

[Go言語]encoding/xmlパッケージの使い方

2023/06/25に公開

はじめに

GoでXML処理を実装する機会があり、そのときに調べて理解したことをまとめました。
XML関連の処理はencoding/xmlパッケージを使います。

XMLとは

Extensible Markup Language (XML) を使用すると、共有可能な方法でデータを定義および保存できます。XML は、ウェブサイト、データベース、サードパーティーアプリケーションなどのコンピュータシステム間における情報のやり取りをサポートします。事前に定義されたルールを使用すると、受信者はこれらのルールを使用してデータを正確かつ効率的に読み取ることができるため、任意のネットワーク上でデータを XML ファイルとして簡単に送信できます。

参考:XML とは何ですか?

XMLと似ているものとしてHTMLがあります。
両者の特徴は次の通りです。

HTML

  • HTMLはデータの提示と表示を目的
  • 表示タグは事前に定義されたもの
  • タグには大文字小文字の区別なし

XML

  • データの格納と送信を目的
  • 独自のタグを定義可能
  • タグには大文字小文字の区別あり(一致しないと構文エラーになる)

構造体からXMLへ変換

Marshal関数もしくはMarshalIndent関数のどちらかでXMLを生成できます。
関数の主な違いは,MarshalIndentが生成するXMLの見やすさをを調整できます。

Marshal

func Marshal(v any) ([]byte, error)

公式

サンプルコード
main.go
package main

import (
	"encoding/xml"
	"fmt"
	"os"
)

type Foods struct {
	XMLName  xml.Name `xml:"foods"`
	Food     []food   `xml:"food"`
	Category string   `xml:"category,attr"`
}

type food struct {
	Name  string `xml:"name"`
	Price string `xml:"price"`
}

func main() {
	v := &Foods{Category: "1"}
	v.Food = append(v.Food, food{Name: "ラーメン", Price: "700円"})
	v.Food = append(v.Food, food{Name: "パスタ", Price: "850円"})
	v.Food = append(v.Food, food{Name: "うどん", Price: "500円"})
	output, err := xml.Marshal(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"?>
<foods category="1"><food><name>ラーメン</name><price>700円</price></food><food><name>パスタ</name><price>850円</price></food><food><name>うどん</name><price>500円</price></food></foods>

Go Playgroundで確認

Marshal関数は何をしているのか

func Marshal(v any) ([]byte, error) {
	var b bytes.Buffer
	enc := NewEncoder(&b)
	if err := enc.Encode(v); err != nil {
		return nil, err
	}
	if err := enc.Close(); err != nil {
		return nil, err
	}
	return b.Bytes(), nil
}

MarshalIndent

func MarshalIndent(v any, prefix, indent string) ([]byte, error)

公式

サンプルコード
main.go
package main

import (
	"encoding/xml"
	"fmt"
	"os"
)

type Foods struct {
	XMLName  xml.Name `xml:"foods"`
	Food     []food   `xml:"food"`
	Category string   `xml:"category,attr"`
}

type food struct {
	Name  string `xml:"name"`
	Price string `xml:"price"`
}

func main() {
	v := &Foods{Category: "1"}
	v.Food = append(v.Food, food{Name: "ラーメン", Price: "700円"})
	v.Food = append(v.Food, food{Name: "パスタ", Price: "850円"})
	v.Food = append(v.Food, food{Name: "うどん", Price: "500円"})
	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"?>
  <foods category="1">
      <food>
          <name>ラーメン</name>
          <price>700円</price>
      </food>
      <food>
          <name>パスタ</name>
          <price>850円</price>
      </food>
      <food>
          <name>うどん</name>
          <price>500円</price>
      </food>
  </foods>

Go Playgroundで確認

MarshalIndent関数は何をしているのか

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
	var b bytes.Buffer
	enc := NewEncoder(&b)
	enc.Indent(prefix, indent)
	if err := enc.Encode(v); err != nil {
		return nil, err
	}
	if err := enc.Close(); err != nil {
		return nil, err
	}
	return b.Bytes(), nil
}

補足

XML処理で知っておくべきこと

XMLヘッダーの定義

[]byte(xml.Header)で定義可能。
MarshalやMarshalIndentの関数ではXMLヘッダー情報を持ちません。
そのため意図的にXMLヘッダーを定義する必要があります。

構造体フィールドタグ

type Foods struct {
	XMLName  xml.Name `xml:"foods"`
	Food     []food   `xml:"food"`
	Category string   `xml:"category,attr"`
}
type food struct {
	Name  string `xml:"name"`
	Price string `xml:"price"`
}
  • XMLNameフィールドは、その構造体で表すXML要素名を定義します
  • XML要素名を構造体のフィールド名ではなく、タグで指定した名前で表示できる(xml:"food"で要素名をfoodに指定)
  • 構造体のフィールド名をXML要素ではなく、XML属性に定義できる(xml:"category,attr"のattrで属性宣言し、属性名をcategoryとする)
  • XML要素の出力順を指定できる(入れ子のときにどの順番で表示するか)
  • フィールドタグによってはXMLには非表示、コメントを出力することなどできます

XMLから構造体へ変換

Unmarshal関数でXMLから構造体を生成できます(構造体以外も生成可)。

Unmarshal

func Unmarshal(data []byte, v any) error

公式

サンプルコード
main.go
package main

import (
	"encoding/xml"
	"fmt"
)

type Foods struct {
	XMLName  xml.Name `xml:"foods"`
	Food     []food   `xml:"food"`
	Category string   `xml:"category,attr"`
}

type food struct {
	Name  string `xml:"name"`
	Price string `xml:"price"`
}

func main() {
	data := `
	<?xml version="1.0" encoding="UTF-8"?>
		<foods category="1">
			<food>
				<name>ラーメン</name>
				<price>700円</price>
			</food>
			<food>
				<name>パスタ</name>
				<price>850円</price>
			</food>
			<food>
				<name>うどん</name>
				<price>500円</price>
			</food>
		</foods>
	`

	v := Foods{}
	err := xml.Unmarshal([]byte(data), &v)
	if err != nil {
		fmt.Printf("error: %v", err)
		return
	}

	fmt.Printf("XMLName: %#v\n", v.XMLName)
	fmt.Printf("Food: %q\n", v.Food)
	fmt.Printf("Category: %q\n", v.Category)
}
出力結果
XMLName: xml.Name{Space:"", Local:"foods"}
Food: [{"ラーメン" "700円"} {"パスタ" "850円"} {"うどん" "500円"}]
Category: "1"

Go Playgroundで確認

Unmarshal関数は何をしているのか

func Unmarshal(data []byte, v any) error {
	return NewDecoder(bytes.NewReader(data)).Decode(v)
}

(おまけ) zennのRSSフィードからトレンドの記事を出力する

zennのRSSフィードはこちら

出力内容は次の通りで実装しています

  • 作成日時
  • 作成者
  • 記事タイトル
  • 記事のリンク
コード
main.go
package main

import (
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"time"
)

type XML struct {
	Articles []struct {
		Title   string `xml:"title"`
		Link    string `xml:"link"`
		Date    string `xml:"pubDate"`
		Creator string `xml:"creator"`
	} `xml:"channel>item"`
}

func main() {
	data := httpGet("https://zenn.dev/feed")

	result := XML{}
	err := xml.Unmarshal([]byte(data), &result)
	if err != nil {
		fmt.Printf("error: %v", err)
		return
	}

	for _, article := range result.Articles {
		datetime, _ := time.Parse("Mon, 02 Jan 2006 15:04:05 MST", article.Date)

		fmt.Printf("%v\n", datetime.Format("2006/01/02 15:04:05"))
		fmt.Printf("%s\n", article.Creator)
		fmt.Printf("%s\n", article.Title)
		fmt.Printf("%v\n", article.Link)
		fmt.Println()
	}
}

func httpGet(url string) string {
	res, _ := http.Get(url)
	body, _ := io.ReadAll(res.Body)
	defer res.Body.Close()
	return string(body)
}
出力結果(一部)
2023/06/25 02:24:22
LisRas
TestFlight リリースversionの変更のやり方 iOS
https://zenn.dev/lisras/articles/bf02fff7fb2760

・・・省略

Discussion