Closed10

Goのencoding/xmlの使い方について雑に紹介

nabeyangnabeyang

xmlをパースする機会は減っているように思いますが、そういった雑用は突然やってくるものです。ここではGo言語の標準ライブラリencoding/xmlでxmlをパースする方法についてzennのRSSを使って雑に紹介します。

nabeyangnabeyang

rssファイルをパースしてout.xmlとして保存するというコードでパースできているか確認していきながら書いていきます。

main.go
package main

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

type ZennRss struct {
}

func main() {
	r, err := os.Open("./feed.xml")
	if err != nil {
		log.Fatal(err)
	}
	defer r.Close()
	var rec ZennRss
	dec := xml.NewDecoder(r)
	err = dec.Decode(&rec)
	if err != nil {
		log.Fatal(err)
	}

	w, err := os.Create("./out.xml")
	if err != nil {
		log.Fatal(err)
	}
	defer w.Close()
	enc := xml.NewEncoder(w)
	enc.Indent("", "  ")
	enc.Encode(&rec)
}

このコードを実行しても、エラーにはなりません。ただしout.xmlの内容は少し以外かもしれません。

out.xml
<ZennRss></ZennRss>

以降ではZennRssの定義のみ修正していきます。

nabeyangnabeyang

トップレベルのタグ名を指定するようにします。

main.go
 type ZennRss struct {
+       XMLName xml.Name `xml:"rss"`
 }

こうすると保存されるファイルは次のように変わります。<?xml...は抜けますが、少しましになりました。

out.xml
<rss></rss>

じつはXMLNameNameとかに変えるとうまく動きません。

out.xml
<ZennRss>
  <rss></rss>
</ZennRss>
nabeyangnabeyang

属性をパースするときはxxx,attrという感じで書いていきます。名前空間は省略しないと空文字列になるようです。

main.go
 type ZennRss struct {
	XMLName      xml.Name `xml:"rss"`
+       XmlnsDC      string   `xml:"dc,attr"`
+       XmlnsContent string   `xml:"content,attr"`
+       XmlnsAtom    string   `xml:"atom,attr"`
+       Version      string   `xml:"version,attr"`
 }

out.xmlは次のようになり、名前空間が消えてしまいます。パースの目的からすると特に問題無いので気にしないことにします。

out.xml
<rss dc="http://purl.org/dc/elements/1.1/" content="http://purl.org/rss/1.0/modules/content/" atom="http://www.w3.org/2005/Atom" version="2.0"></rss>

試しにxml:"rss"xml:"xmlns:dcに変えると空になります。

out.xml
<rss xmlns:dc="" content="http://purl.org/rss/1.0/modules/content/" atom="http://www.w3.org/2005/Atom" version="2.0"></rss>
nabeyangnabeyang

channel内のtitleの部分を取ってみます。

main.go
@@ -12,6 +12,9 @@ type ZennRss struct {
        XmlnsContent string   `xml:"content,attr"`
        XmlnsAtom    string   `xml:"atom,attr"`
        Version      string   `xml:"version,attr"`
+       Channel      struct {
+               Title string `xml:"title"`
+       } `xml:"channel"`
 }

<![CDATA[..]]>は特に意識しなくても読んでくれるようです。ただし、out.xmlには自動的にはCDATAは付きません。

out.xml
<rss dc="http://purl.org/dc/elements/1.1/" content="http://purl.org/rss/1.0/modules/content/" atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>Zennの「Zenn」のフィード</title>
  </channel>
</rss>
nabeyangnabeyang

色々飛ばしてitemを取ってみます。これは連続してあるので配列とすると、すべての要素が取れます。

main.go
@@ -14,6 +14,15 @@ type ZennRss struct {
        Version      string   `xml:"version,attr"`
        Channel      struct {
                Title string `xml:"title"`
+               Items []struct {
+                       Title       string `xml:"title"`
+                       Description string `xml:"description"`
+                       Link        string `xml:"link"`
+                       GuID        string `xml:"guid"`
+                       PubDate     string `xml:"pubDate"`
+                       Enclosure   string `xml:"enclosure"`
+                       Creator     string `xml:"creator"`
+               } `xml:"item"`
nabeyangnabeyang

out.xmlを見ると、descriptionのCDATAが付いてないですね。パースのみを考えるとCDATA付ける必要ないですが、次のように変更するとCDATAで囲んでくれます。

main.go
@@ -16,12 +16,15 @@ type ZennRss struct {
                Title string `xml:"title"`
                Items []struct {
                        Title       string `xml:"title"`
+                       Description struct {
+                               XMLName xml.Name `xml:"description"`
+                               Value   string   `xml:",cdata"`
+                       }
nabeyangnabeyang

item内のguidisPermaLinkが取れてないのが不満ですね。タグの中のデータと属性を同時に取るパターンは新しいですが、次のようにすれば取れます。

main.go
@@ -20,8 +20,12 @@ type ZennRss struct {
                                XMLName xml.Name `xml:"description"`
                                Value   string   `xml:",cdata"`
                        }
                       Link string `xml:"link"`
-                       GuID      string `xml:"guid"`
+                       GuID struct {
+                               XMLName     xml.Name `xml:"guid"`
+                               IsPermaLink bool     `xml:"isPermaLink,attr"`
+                               Value       string   `xml:",chardata"`
+                       }
                        PubDate   string `xml:"pubDate"`
nabeyangnabeyang

feed.xmlを見ながら、上に書いたのと同じパターンの繰り返しでZennRssは出来ます。

main.go
package main

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

type ZennRss struct {
	XMLName      xml.Name `xml:"rss"`
	XmlnsDC      string   `xml:"dc,attr"`
	XmlnsContent string   `xml:"content,attr"`
	XmlnsAtom    string   `xml:"atom,attr"`
	Version      string   `xml:"version,attr"`
	Channel      struct {
		Title       string `xml:"title"`
		Description string `xml:"description"`
		Link        string `xml:"_ link"`
		Image       struct {
			Url   string `xml:"url"`
			Title string `xml:"title"`
			Link  string `xml:"link"`
		} `xml:"image"`
		Generator     string `xml:"generator"`
		LastBuildDate string `xml:"lastBuildDate"`
		AtomLink      struct {
			XMLName xml.Name `xml:"http://www.w3.org/2005/Atom link"`
			Href    string   `xml:"href,attr"`
			Rel     string   `xml:"rel,attr"`
			Type    string   `xml:"type,attr"`
		}
		Language string `xml:"language"`
		Items    []struct {
			Title       string `xml:"title"`
			Description struct {
				XMLName xml.Name `xml:"description"`
				Value   string   `xml:",cdata"`
			}
			Link string `xml:"link"`
			GuID struct {
				XMLName     xml.Name `xml:"guid"`
				IsPermaLink bool     `xml:"isPermaLink,attr"`
				Value       string   `xml:",chardata"`
			}
			PubDate   string `xml:"pubDate"`
			Enclosure struct {
				XMLName string `xml:"enclosure"`
				Url     string `xml:"url,attr"`
				Length  int    `xml:"length,attr"`
				Type    string `xml:"type,attr"`
			}
			Creator string `xml:"creator"`
		} `xml:"item"`
	} `xml:"channel"`
}

func main() {
	r, err := os.Open("./feed.xml")
	if err != nil {
		log.Fatal(err)
	}
	defer r.Close()
	var rec ZennRss
	dec := xml.NewDecoder(r)
	dec.DefaultSpace = "_"
	err = dec.Decode(&rec)
	if err != nil {
		log.Fatal(err)
	}

	w, err := os.Create("./out.xml")
	if err != nil {
		log.Fatal(err)
	}
	defer w.Close()
	enc := xml.NewEncoder(w)
	enc.Indent("", "  ")
	enc.Encode(&rec)
}

xml:"_ link"xml:"http://www.w3.org/2005/Atom link"は今までの書き方と違っていて、main関数でもdec.DefaultSpace = "_"が1行追加されています。このように名前空間を扱わないとlink同士でコンフリクトを起こします。出来上がるout.xmlは多少おかしいところもありますが、パースの方はうまくできています。

nabeyangnabeyang

実用的にはxml:"item"xml:"channel>item"などとすると構造体の階層を深く作らなくても大丈夫です。

type ZennRss struct {
	XMLName xml.Name `xml:"rss"`
	Items   []struct {
		Title       string `xml:"title"`
		Description struct {
			XMLName xml.Name `xml:"description"`
			Value   string   `xml:",cdata"`
		}
		Link string `xml:"link"`
		GuID struct {
			XMLName     xml.Name `xml:"guid"`
			IsPermaLink bool     `xml:"isPermaLink,attr"`
			Value       string   `xml:",chardata"`
		}
		PubDate   string `xml:"pubDate"`
		Enclosure struct {
			XMLName string `xml:"enclosure"`
			Url     string `xml:"url,attr"`
			Length  int    `xml:"length,attr"`
			Type    string `xml:"type,attr"`
		}
		Creator string `xml:"creator"`
	} `xml:"channel>item"`
}
このスクラップは2021/04/03にクローズされました